Merge branch 'develop' into ignore-mandatory-in-payment-reconcilitation
This commit is contained in:
commit
c86656f499
@ -98,8 +98,6 @@ rules:
|
||||
languages: [python]
|
||||
severity: WARNING
|
||||
paths:
|
||||
exclude:
|
||||
- test_*.py
|
||||
include:
|
||||
- "*/**/doctype/*"
|
||||
|
||||
|
36
.github/workflows/semgrep.yml
vendored
36
.github/workflows/semgrep.yml
vendored
@ -1,34 +1,18 @@
|
||||
name: Semgrep
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- develop
|
||||
- version-13-hotfix
|
||||
- version-13-pre-release
|
||||
pull_request: { }
|
||||
|
||||
jobs:
|
||||
semgrep:
|
||||
name: Frappe Linter
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Setup python3
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.8
|
||||
|
||||
- name: Setup semgrep
|
||||
run: |
|
||||
python -m pip install -q semgrep
|
||||
git fetch origin $GITHUB_BASE_REF:$GITHUB_BASE_REF -q
|
||||
|
||||
- name: Semgrep errors
|
||||
run: |
|
||||
files=$(git diff --name-only --diff-filter=d $GITHUB_BASE_REF)
|
||||
[[ -d .github/helper/semgrep_rules ]] && semgrep --severity ERROR --config=.github/helper/semgrep_rules --quiet --error $files
|
||||
semgrep --config="r/python.lang.correctness" --quiet --error $files
|
||||
|
||||
- name: Semgrep warnings
|
||||
run: |
|
||||
files=$(git diff --name-only --diff-filter=d $GITHUB_BASE_REF)
|
||||
[[ -d .github/helper/semgrep_rules ]] && semgrep --severity WARNING --severity INFO --config=.github/helper/semgrep_rules --quiet $files
|
||||
- uses: actions/checkout@v2
|
||||
- uses: returntocorp/semgrep-action@v1
|
||||
env:
|
||||
SEMGREP_TIMEOUT: 120
|
||||
with:
|
||||
config: >-
|
||||
r/python.lang.correctness
|
||||
.github/helper/semgrep_rules
|
||||
|
43
CODEOWNERS
43
CODEOWNERS
@ -3,16 +3,33 @@
|
||||
# These owners will be the default owners for everything in
|
||||
# the repo. Unless a later match takes precedence,
|
||||
|
||||
manufacturing/ @rohitwaghchaure @marination
|
||||
accounts/ @deepeshgarg007 @nextchamp-saqib
|
||||
loan_management/ @deepeshgarg007 @rohitwaghchaure
|
||||
pos* @nextchamp-saqib @rohitwaghchaure
|
||||
assets/ @nextchamp-saqib @deepeshgarg007
|
||||
stock/ @marination @rohitwaghchaure
|
||||
buying/ @marination @deepeshgarg007
|
||||
hr/ @Anurag810 @rohitwaghchaure
|
||||
projects/ @hrwX @nextchamp-saqib
|
||||
support/ @hrwX @marination
|
||||
healthcare/ @ruchamahabal @marination
|
||||
erpnext_integrations/ @Mangesh-Khairnar @nextchamp-saqib
|
||||
requirements.txt @gavindsouza
|
||||
erpnext/accounts/ @nextchamp-saqib @deepeshgarg007
|
||||
erpnext/assets/ @nextchamp-saqib @deepeshgarg007
|
||||
erpnext/erpnext_integrations/ @nextchamp-saqib
|
||||
erpnext/loan_management/ @nextchamp-saqib @deepeshgarg007
|
||||
erpnext/regional @nextchamp-saqib @deepeshgarg007
|
||||
erpnext/selling @nextchamp-saqib @deepeshgarg007
|
||||
erpnext/support/ @nextchamp-saqib @deepeshgarg007
|
||||
pos* @nextchamp-saqib
|
||||
|
||||
erpnext/buying/ @marination @rohitwaghchaure @ankush
|
||||
erpnext/e_commerce/ @marination
|
||||
erpnext/maintenance/ @marination @rohitwaghchaure
|
||||
erpnext/manufacturing/ @marination @rohitwaghchaure @ankush
|
||||
erpnext/portal/ @marination
|
||||
erpnext/quality_management/ @marination @rohitwaghchaure
|
||||
erpnext/shopping_cart/ @marination
|
||||
erpnext/stock/ @marination @rohitwaghchaure @ankush
|
||||
|
||||
erpnext/crm/ @ruchamahabal
|
||||
erpnext/education/ @ruchamahabal
|
||||
erpnext/healthcare/ @ruchamahabal
|
||||
erpnext/hr/ @ruchamahabal
|
||||
erpnext/non_profit/ @ruchamahabal
|
||||
erpnext/payroll @ruchamahabal
|
||||
erpnext/projects/ @ruchamahabal
|
||||
|
||||
erpnext/controllers @deepeshgarg007 @nextchamp-saqib @rohitwaghchaure @marination
|
||||
|
||||
.github/ @surajshetty3416 @ankush
|
||||
requirements.txt @gavindsouza
|
||||
|
@ -5,7 +5,7 @@ import frappe
|
||||
from erpnext.hooks import regional_overrides
|
||||
from frappe.utils import getdate
|
||||
|
||||
__version__ = '13.6.0'
|
||||
__version__ = '13.7.0'
|
||||
|
||||
def get_default_company(user=None):
|
||||
'''Get default company for user'''
|
||||
|
@ -16,7 +16,6 @@ class ChartofAccountsImporter(Document):
|
||||
def validate(self):
|
||||
validate_accounts(self.import_file)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def validate_company(company):
|
||||
parent_company, allow_account_creation_against_child_company = frappe.db.get_value('Company',
|
||||
@ -307,7 +306,6 @@ def validate_accounts(file_name):
|
||||
|
||||
validate_account_types(accounts_dict)
|
||||
|
||||
|
||||
return [True, len(accounts)]
|
||||
|
||||
def validate_root(accounts):
|
||||
|
@ -139,7 +139,7 @@ def create_dunning_type_with_zero_interest_rate():
|
||||
dunning_type.append(
|
||||
"dunning_letter_text", {
|
||||
'language': 'en',
|
||||
'body_text': 'We have still not received payment for our invoice ',
|
||||
'body_text': 'We have still not received payment for our invoice ',
|
||||
'closing_text': 'We kindly request that you pay the outstanding amount immediately, and late fees.'
|
||||
}
|
||||
)
|
||||
|
@ -667,6 +667,7 @@
|
||||
{
|
||||
"fieldname": "base_paid_amount_after_tax",
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 1,
|
||||
"label": "Paid Amount After Tax (Company Currency)",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
@ -693,21 +694,25 @@
|
||||
"depends_on": "eval:doc.received_amount && doc.payment_type != 'Internal Transfer'",
|
||||
"fieldname": "received_amount_after_tax",
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 1,
|
||||
"label": "Received Amount After Tax",
|
||||
"options": "paid_to_account_currency"
|
||||
"options": "paid_to_account_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "doc.received_amount",
|
||||
"fieldname": "base_received_amount_after_tax",
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 1,
|
||||
"label": "Received Amount After Tax (Company Currency)",
|
||||
"options": "Company:company:default_currency"
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-06-22 20:37:06.154206",
|
||||
"modified": "2021-07-09 08:58:15.008761",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Entry",
|
||||
|
@ -411,9 +411,15 @@ class PaymentEntry(AccountsController):
|
||||
if not self.advance_tax_account:
|
||||
frappe.throw(_("Advance TDS account is mandatory for advance TDS deduction"))
|
||||
|
||||
reference_doclist = []
|
||||
net_total = self.paid_amount
|
||||
included_in_paid_amount = 0
|
||||
|
||||
for reference in self.get("references"):
|
||||
net_total_for_tds = 0
|
||||
if reference.reference_doctype == 'Purchase Order':
|
||||
net_total_for_tds += flt(frappe.db.get_value('Purchase Order', reference.reference_name, 'net_total'))
|
||||
|
||||
if net_total_for_tds:
|
||||
net_total = net_total_for_tds
|
||||
|
||||
# Adding args as purchase invoice to get TDS amount
|
||||
args = frappe._dict({
|
||||
@ -430,7 +436,7 @@ class PaymentEntry(AccountsController):
|
||||
return
|
||||
|
||||
tax_withholding_details.update({
|
||||
'included_in_paid_amount': included_in_paid_amount,
|
||||
'add_deduct_tax': 'Add',
|
||||
'cost_center': self.cost_center or erpnext.get_default_cost_center(self.company)
|
||||
})
|
||||
|
||||
@ -519,16 +525,19 @@ class PaymentEntry(AccountsController):
|
||||
self.unallocated_amount = 0
|
||||
if self.party:
|
||||
total_deductions = sum(flt(d.amount) for d in self.get("deductions"))
|
||||
included_taxes = self.get_included_taxes()
|
||||
if self.payment_type == "Receive" \
|
||||
and self.base_total_allocated_amount < self.base_received_amount_after_tax + total_deductions \
|
||||
and self.total_allocated_amount < self.paid_amount_after_tax + (total_deductions / self.source_exchange_rate):
|
||||
self.unallocated_amount = (self.received_amount_after_tax + total_deductions -
|
||||
and self.base_total_allocated_amount < self.base_received_amount + total_deductions \
|
||||
and self.total_allocated_amount < self.paid_amount + (total_deductions / self.source_exchange_rate):
|
||||
self.unallocated_amount = (self.received_amount + total_deductions -
|
||||
self.base_total_allocated_amount) / self.source_exchange_rate
|
||||
self.unallocated_amount -= included_taxes
|
||||
elif self.payment_type == "Pay" \
|
||||
and self.base_total_allocated_amount < (self.base_paid_amount_after_tax - total_deductions) \
|
||||
and self.total_allocated_amount < self.received_amount_after_tax + (total_deductions / self.target_exchange_rate):
|
||||
self.unallocated_amount = (self.base_paid_amount_after_tax - (total_deductions +
|
||||
and self.base_total_allocated_amount < (self.base_paid_amount - total_deductions) \
|
||||
and self.total_allocated_amount < self.received_amount + (total_deductions / self.target_exchange_rate):
|
||||
self.unallocated_amount = (self.base_paid_amount - (total_deductions +
|
||||
self.base_total_allocated_amount)) / self.target_exchange_rate
|
||||
self.unallocated_amount -= included_taxes
|
||||
|
||||
def set_difference_amount(self):
|
||||
base_unallocated_amount = flt(self.unallocated_amount) * (flt(self.source_exchange_rate)
|
||||
@ -537,17 +546,29 @@ class PaymentEntry(AccountsController):
|
||||
base_party_amount = flt(self.base_total_allocated_amount) + flt(base_unallocated_amount)
|
||||
|
||||
if self.payment_type == "Receive":
|
||||
self.difference_amount = base_party_amount - self.base_received_amount_after_tax
|
||||
self.difference_amount = base_party_amount - self.base_received_amount
|
||||
elif self.payment_type == "Pay":
|
||||
self.difference_amount = self.base_paid_amount_after_tax - base_party_amount
|
||||
self.difference_amount = self.base_paid_amount - base_party_amount
|
||||
else:
|
||||
self.difference_amount = self.base_paid_amount_after_tax - flt(self.base_received_amount_after_tax)
|
||||
self.difference_amount = self.base_paid_amount - flt(self.base_received_amount)
|
||||
|
||||
total_deductions = sum(flt(d.amount) for d in self.get("deductions"))
|
||||
included_taxes = self.get_included_taxes()
|
||||
|
||||
self.difference_amount = flt(self.difference_amount - total_deductions,
|
||||
self.difference_amount = flt(self.difference_amount - total_deductions - included_taxes,
|
||||
self.precision("difference_amount"))
|
||||
|
||||
def get_included_taxes(self):
|
||||
included_taxes = 0
|
||||
for tax in self.get('taxes'):
|
||||
if tax.included_in_paid_amount:
|
||||
if tax.add_deduct_tax == 'Add':
|
||||
included_taxes += tax.base_tax_amount
|
||||
else:
|
||||
included_taxes -= tax.base_tax_amount
|
||||
|
||||
return included_taxes
|
||||
|
||||
# Paid amount is auto allocated in the reference document by default.
|
||||
# Clear the reference document which doesn't have allocated amount on validate so that form can be loaded fast
|
||||
def clear_unallocated_reference_document_rows(self):
|
||||
@ -690,8 +711,8 @@ class PaymentEntry(AccountsController):
|
||||
"account": self.paid_from,
|
||||
"account_currency": self.paid_from_account_currency,
|
||||
"against": self.party if self.payment_type=="Pay" else self.paid_to,
|
||||
"credit_in_account_currency": self.paid_amount_after_tax,
|
||||
"credit": self.base_paid_amount_after_tax,
|
||||
"credit_in_account_currency": self.paid_amount,
|
||||
"credit": self.base_paid_amount,
|
||||
"cost_center": self.cost_center
|
||||
}, item=self)
|
||||
)
|
||||
@ -701,8 +722,8 @@ class PaymentEntry(AccountsController):
|
||||
"account": self.paid_to,
|
||||
"account_currency": self.paid_to_account_currency,
|
||||
"against": self.party if self.payment_type=="Receive" else self.paid_from,
|
||||
"debit_in_account_currency": self.received_amount_after_tax,
|
||||
"debit": self.base_received_amount_after_tax,
|
||||
"debit_in_account_currency": self.received_amount,
|
||||
"debit": self.base_received_amount,
|
||||
"cost_center": self.cost_center
|
||||
}, item=self)
|
||||
)
|
||||
@ -715,35 +736,42 @@ class PaymentEntry(AccountsController):
|
||||
|
||||
if self.payment_type in ('Pay', 'Internal Transfer'):
|
||||
dr_or_cr = "debit" if d.add_deduct_tax == "Add" else "credit"
|
||||
against = self.party or self.paid_from
|
||||
elif self.payment_type == 'Receive':
|
||||
dr_or_cr = "credit" if d.add_deduct_tax == "Add" else "debit"
|
||||
against = self.party or self.paid_to
|
||||
|
||||
payment_or_advance_account = self.get_party_account_for_taxes()
|
||||
tax_amount = d.tax_amount
|
||||
base_tax_amount = d.base_tax_amount
|
||||
|
||||
if self.advance_tax_account:
|
||||
tax_amount = -1 * tax_amount
|
||||
base_tax_amount = -1 * base_tax_amount
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict({
|
||||
"account": d.account_head,
|
||||
"against": self.party if self.payment_type=="Receive" else self.paid_from,
|
||||
dr_or_cr: d.base_tax_amount,
|
||||
dr_or_cr + "_in_account_currency": d.base_tax_amount
|
||||
"against": against,
|
||||
dr_or_cr: tax_amount,
|
||||
dr_or_cr + "_in_account_currency": base_tax_amount
|
||||
if account_currency==self.company_currency
|
||||
else d.tax_amount,
|
||||
"cost_center": d.cost_center
|
||||
}, account_currency, item=d))
|
||||
|
||||
#Intentionally use -1 to get net values in party account
|
||||
gl_entries.append(
|
||||
self.get_gl_dict({
|
||||
"account": payment_or_advance_account,
|
||||
"against": self.party if self.payment_type=="Receive" else self.paid_from,
|
||||
dr_or_cr: -1 * d.base_tax_amount,
|
||||
dr_or_cr + "_in_account_currency": -1*d.base_tax_amount
|
||||
if account_currency==self.company_currency
|
||||
else d.tax_amount,
|
||||
"cost_center": self.cost_center,
|
||||
"party_type": self.party_type,
|
||||
"party": self.party
|
||||
}, account_currency, item=d))
|
||||
if not d.included_in_paid_amount or self.advance_tax_account:
|
||||
gl_entries.append(
|
||||
self.get_gl_dict({
|
||||
"account": payment_or_advance_account,
|
||||
"against": against,
|
||||
dr_or_cr: -1 * tax_amount,
|
||||
dr_or_cr + "_in_account_currency": -1 * base_tax_amount
|
||||
if account_currency==self.company_currency
|
||||
else d.tax_amount,
|
||||
"cost_center": self.cost_center,
|
||||
}, account_currency, item=d))
|
||||
|
||||
def add_deductions_gl_entries(self, gl_entries):
|
||||
for d in self.get("deductions"):
|
||||
@ -767,9 +795,9 @@ class PaymentEntry(AccountsController):
|
||||
if self.advance_tax_account:
|
||||
return self.advance_tax_account
|
||||
elif self.payment_type == 'Receive':
|
||||
return self.paid_from
|
||||
elif self.payment_type in ('Pay', 'Internal Transfer'):
|
||||
return self.paid_to
|
||||
elif self.payment_type in ('Pay', 'Internal Transfer'):
|
||||
return self.paid_from
|
||||
|
||||
def update_advance_paid(self):
|
||||
if self.payment_type in ("Receive", "Pay") and self.party:
|
||||
@ -1648,12 +1676,6 @@ def set_paid_amount_and_received_amount(dt, party_account_currency, bank, outsta
|
||||
if dt == "Employee Advance":
|
||||
paid_amount = received_amount * doc.get('exchange_rate', 1)
|
||||
|
||||
if dt == "Purchase Order" and doc.apply_tds:
|
||||
if party_account_currency == bank.account_currency:
|
||||
paid_amount = received_amount = doc.base_net_total
|
||||
else:
|
||||
paid_amount = received_amount = doc.base_net_total * doc.get('exchange_rate', 1)
|
||||
|
||||
return paid_amount, received_amount
|
||||
|
||||
def apply_early_payment_discount(paid_amount, received_amount, doc):
|
||||
|
@ -975,8 +975,17 @@ class TestPurchaseInvoice(unittest.TestCase):
|
||||
acc_settings.save()
|
||||
|
||||
def test_gain_loss_with_advance_entry(self):
|
||||
unlink_enabled = frappe.db.get_value("Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice")
|
||||
frappe.db.set_value("Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice", 1)
|
||||
unlink_enabled = frappe.db.get_value(
|
||||
"Accounts Settings", "Accounts Settings",
|
||||
"unlink_payment_on_cancel_of_invoice")
|
||||
|
||||
frappe.db.set_value(
|
||||
"Accounts Settings", "Accounts Settings",
|
||||
"unlink_payment_on_cancel_of_invoice", 1)
|
||||
|
||||
original_account = frappe.db.get_value("Company", "_Test Company", "exchange_gain_loss_account")
|
||||
frappe.db.set_value("Company", "_Test Company", "exchange_gain_loss_account", "Exchange Gain/Loss - _TC")
|
||||
|
||||
pay = frappe.get_doc({
|
||||
'doctype': 'Payment Entry',
|
||||
'company': '_Test Company',
|
||||
@ -1016,7 +1025,8 @@ class TestPurchaseInvoice(unittest.TestCase):
|
||||
gl_entries = frappe.db.sql("""
|
||||
select account, sum(debit - credit) as balance from `tabGL Entry`
|
||||
where voucher_no=%s
|
||||
group by account order by account asc""", (pi.name), as_dict=1)
|
||||
group by account
|
||||
order by account asc""", (pi.name), as_dict=1)
|
||||
|
||||
for i, gle in enumerate(gl_entries):
|
||||
self.assertEqual(expected_gle[i][0], gle.account)
|
||||
@ -1076,6 +1086,7 @@ class TestPurchaseInvoice(unittest.TestCase):
|
||||
pay.cancel()
|
||||
|
||||
frappe.db.set_value("Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice", unlink_enabled)
|
||||
frappe.db.set_value("Company", "_Test Company", "exchange_gain_loss_account", original_account)
|
||||
|
||||
def test_purchase_invoice_advance_taxes(self):
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||
|
@ -13,7 +13,7 @@ from erpnext.accounts.utils import get_account_currency
|
||||
from erpnext.stock.doctype.delivery_note.delivery_note import update_billed_amount_based_on_so
|
||||
from erpnext.projects.doctype.timesheet.timesheet import get_projectwise_timesheet_data
|
||||
from erpnext.assets.doctype.asset.depreciation \
|
||||
import get_disposal_account_and_cost_center, get_gl_entries_on_asset_disposal
|
||||
import get_disposal_account_and_cost_center, get_gl_entries_on_asset_disposal, get_gl_entries_on_asset_regain
|
||||
from erpnext.stock.doctype.batch.batch import set_batch_nos
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos, get_delivery_note_serial_no
|
||||
from erpnext.setup.doctype.company.company import update_company_current_month_sales
|
||||
@ -149,7 +149,7 @@ class SalesInvoice(SellingController):
|
||||
if self.update_stock:
|
||||
frappe.throw(_("'Update Stock' cannot be checked for fixed asset sale"))
|
||||
|
||||
elif asset.status in ("Scrapped", "Cancelled", "Sold"):
|
||||
elif asset.status in ("Scrapped", "Cancelled") or (asset.status == "Sold" and not self.is_return):
|
||||
frappe.throw(_("Row #{0}: Asset {1} cannot be submitted, it is already {2}").format(d.idx, d.asset, asset.status))
|
||||
|
||||
def validate_item_cost_centers(self):
|
||||
@ -918,22 +918,33 @@ class SalesInvoice(SellingController):
|
||||
for item in self.get("items"):
|
||||
if flt(item.base_net_amount, item.precision("base_net_amount")):
|
||||
if item.is_fixed_asset:
|
||||
asset = frappe.get_doc("Asset", item.asset)
|
||||
|
||||
if item.get('asset'):
|
||||
asset = frappe.get_doc("Asset", item.asset)
|
||||
else:
|
||||
frappe.throw(_(
|
||||
"Row #{0}: You must select an Asset for Item {1}.").format(item.idx, item.item_name),
|
||||
title=_("Missing Asset")
|
||||
)
|
||||
if (len(asset.finance_books) > 1 and not item.finance_book
|
||||
and asset.finance_books[0].finance_book):
|
||||
frappe.throw(_("Select finance book for the item {0} at row {1}")
|
||||
.format(item.item_code, item.idx))
|
||||
|
||||
fixed_asset_gl_entries = get_gl_entries_on_asset_disposal(asset,
|
||||
item.base_net_amount, item.finance_book)
|
||||
if self.is_return:
|
||||
fixed_asset_gl_entries = get_gl_entries_on_asset_regain(asset,
|
||||
item.base_net_amount, item.finance_book)
|
||||
asset.db_set("disposal_date", None)
|
||||
else:
|
||||
fixed_asset_gl_entries = get_gl_entries_on_asset_disposal(asset,
|
||||
item.base_net_amount, item.finance_book)
|
||||
asset.db_set("disposal_date", self.posting_date)
|
||||
|
||||
for gle in fixed_asset_gl_entries:
|
||||
gle["against"] = self.customer
|
||||
gl_entries.append(self.get_gl_dict(gle, item=item))
|
||||
|
||||
asset.db_set("disposal_date", self.posting_date)
|
||||
asset.set_status("Sold" if self.docstatus==1 else None)
|
||||
self.set_asset_status(asset)
|
||||
|
||||
else:
|
||||
# Do not book income for transfer within same company
|
||||
if not self.is_internal_transfer():
|
||||
@ -959,6 +970,12 @@ class SalesInvoice(SellingController):
|
||||
erpnext.is_perpetual_inventory_enabled(self.company):
|
||||
gl_entries += super(SalesInvoice, self).get_gl_entries()
|
||||
|
||||
def set_asset_status(self, asset):
|
||||
if self.is_return:
|
||||
asset.set_status()
|
||||
else:
|
||||
asset.set_status("Sold" if self.docstatus==1 else None)
|
||||
|
||||
def make_loyalty_point_redemption_gle(self, gl_entries):
|
||||
if cint(self.redeem_loyalty_points):
|
||||
gl_entries.append(
|
||||
|
@ -10,6 +10,7 @@ from frappe.model.dynamic_links import get_dynamic_link_map
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry, get_qty_after_transaction
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import unlink_payment_on_cancel_of_invoice
|
||||
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
|
||||
from erpnext.assets.doctype.asset.test_asset import create_asset, create_asset_data
|
||||
from erpnext.exceptions import InvalidAccountCurrency, InvalidCurrency
|
||||
from erpnext.stock.doctype.serial_no.serial_no import SerialNoWarehouseError
|
||||
from frappe.model.naming import make_autoname
|
||||
@ -1069,6 +1070,36 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
self.assertFalse(si1.outstanding_amount)
|
||||
self.assertEqual(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"), 1500)
|
||||
|
||||
def test_gle_made_when_asset_is_returned(self):
|
||||
create_asset_data()
|
||||
asset = create_asset(item_code="Macbook Pro")
|
||||
|
||||
si = create_sales_invoice(item_code="Macbook Pro", asset=asset.name, qty=1, rate=90000)
|
||||
return_si = create_sales_invoice(is_return=1, return_against=si.name, item_code="Macbook Pro", asset=asset.name, qty=-1, rate=90000)
|
||||
|
||||
disposal_account = frappe.get_cached_value("Company", "_Test Company", "disposal_account")
|
||||
|
||||
# Asset value is 100,000 but it was sold for 90,000, so there should be a loss of 10,000
|
||||
loss_for_si = frappe.get_all(
|
||||
"GL Entry",
|
||||
filters = {
|
||||
"voucher_no": si.name,
|
||||
"account": disposal_account
|
||||
},
|
||||
fields = ["credit", "debit"]
|
||||
)[0]
|
||||
|
||||
loss_for_return_si = frappe.get_all(
|
||||
"GL Entry",
|
||||
filters = {
|
||||
"voucher_no": return_si.name,
|
||||
"account": disposal_account
|
||||
},
|
||||
fields = ["credit", "debit"]
|
||||
)[0]
|
||||
|
||||
self.assertEqual(loss_for_si['credit'], loss_for_return_si['debit'])
|
||||
self.assertEqual(loss_for_si['debit'], loss_for_return_si['credit'])
|
||||
|
||||
def test_discount_on_net_total(self):
|
||||
si = frappe.copy_doc(test_records[2])
|
||||
@ -2087,9 +2118,9 @@ def make_sales_invoice_for_ewaybill():
|
||||
if not gst_account:
|
||||
gst_settings.append("gst_accounts", {
|
||||
"company": "_Test Company",
|
||||
"cgst_account": "CGST - _TC",
|
||||
"sgst_account": "SGST - _TC",
|
||||
"igst_account": "IGST - _TC",
|
||||
"cgst_account": "Output Tax CGST - _TC",
|
||||
"sgst_account": "Output Tax SGST - _TC",
|
||||
"igst_account": "Output Tax IGST - _TC",
|
||||
})
|
||||
|
||||
gst_settings.save()
|
||||
@ -2106,7 +2137,7 @@ def make_sales_invoice_for_ewaybill():
|
||||
|
||||
si.append("taxes", {
|
||||
"charge_type": "On Net Total",
|
||||
"account_head": "CGST - _TC",
|
||||
"account_head": "Output Tax CGST - _TC",
|
||||
"cost_center": "Main - _TC",
|
||||
"description": "CGST @ 9.0",
|
||||
"rate": 9
|
||||
@ -2114,7 +2145,7 @@ def make_sales_invoice_for_ewaybill():
|
||||
|
||||
si.append("taxes", {
|
||||
"charge_type": "On Net Total",
|
||||
"account_head": "SGST - _TC",
|
||||
"account_head": "Output Tax SGST - _TC",
|
||||
"cost_center": "Main - _TC",
|
||||
"description": "SGST @ 9.0",
|
||||
"rate": 9
|
||||
@ -2164,6 +2195,7 @@ def create_sales_invoice(**args):
|
||||
"rate": args.rate if args.get("rate") is not None else 100,
|
||||
"income_account": args.income_account or "Sales - _TC",
|
||||
"expense_account": args.expense_account or "Cost of Goods Sold - _TC",
|
||||
"asset": args.asset or None,
|
||||
"cost_center": args.cost_center or "_Test Cost Center - _TC",
|
||||
"serial_no": args.serial_no,
|
||||
"conversion_factor": 1
|
||||
|
@ -743,7 +743,6 @@
|
||||
"fieldname": "asset",
|
||||
"fieldtype": "Link",
|
||||
"label": "Asset",
|
||||
"no_copy": 1,
|
||||
"options": "Asset"
|
||||
},
|
||||
{
|
||||
@ -826,7 +825,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-02-23 01:05:22.123527",
|
||||
"modified": "2021-06-21 23:03:11.599901",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice Item",
|
||||
|
@ -1,24 +1,6 @@
|
||||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
// License: GNU General Public License v3. See license.txt
|
||||
|
||||
cur_frm.add_fetch("customer", "customer_group", "customer_group" );
|
||||
cur_frm.add_fetch("supplier", "supplier_group_name", "supplier_group" );
|
||||
|
||||
frappe.ui.form.on("Tax Rule", "tax_type", function(frm) {
|
||||
frm.toggle_reqd("sales_tax_template", frm.doc.tax_type=="Sales");
|
||||
frm.toggle_reqd("purchase_tax_template", frm.doc.tax_type=="Purchase");
|
||||
})
|
||||
|
||||
frappe.ui.form.on("Tax Rule", "onload", function(frm) {
|
||||
if(frm.doc.__islocal) {
|
||||
frm.set_value("use_for_shopping_cart", 1);
|
||||
}
|
||||
})
|
||||
|
||||
frappe.ui.form.on("Tax Rule", "refresh", function(frm) {
|
||||
frappe.ui.form.trigger("Tax Rule", "tax_type");
|
||||
})
|
||||
|
||||
frappe.ui.form.on("Tax Rule", "customer", function(frm) {
|
||||
if(frm.doc.customer) {
|
||||
frappe.call({
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -50,7 +50,7 @@ class TestTaxRule(unittest.TestCase):
|
||||
tax_rule1 = make_tax_rule(customer_group= "All Customer Groups",
|
||||
sales_tax_template = "_Test Sales Taxes and Charges Template - _TC", priority = 1, from_date = "2015-01-01")
|
||||
tax_rule1.save()
|
||||
self.assertEqual(get_tax_template("2015-01-01", {"customer_group" : "Commercial", "use_for_shopping_cart":0}),
|
||||
self.assertEqual(get_tax_template("2015-01-01", {"customer_group" : "Commercial", "use_for_shopping_cart":1}),
|
||||
"_Test Sales Taxes and Charges Template - _TC")
|
||||
|
||||
def test_conflict_with_overlapping_dates(self):
|
||||
|
@ -55,9 +55,11 @@ 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')
|
||||
and account_details[filters.account].is_group == 0):
|
||||
frappe.throw(_("Can not filter based on Account, if grouped 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')]):
|
||||
|
@ -75,7 +75,8 @@ def get_invoice_and_tds_amount(supplier, account, company, from_date, to_date, f
|
||||
select voucher_no, credit
|
||||
from `tabGL Entry`
|
||||
where party in (%s) and credit > 0
|
||||
and company=%s and posting_date between %s and %s
|
||||
and company=%s and is_cancelled = 0
|
||||
and posting_date between %s and %s
|
||||
""", (supplier, company, from_date, to_date), as_dict=1)
|
||||
|
||||
supplier_credit_amount = flt(sum(d.credit for d in entries))
|
||||
|
@ -788,7 +788,7 @@ def get_children(doctype, parent, company, is_root=False):
|
||||
return acc
|
||||
|
||||
def create_payment_gateway_account(gateway, payment_channel="Email"):
|
||||
from erpnext.setup.setup_wizard.operations.company_setup import create_bank_account
|
||||
from erpnext.setup.setup_wizard.operations.install_fixtures import create_bank_account
|
||||
|
||||
company = frappe.db.get_value("Global Defaults", None, "default_company")
|
||||
if not company:
|
||||
|
@ -176,22 +176,34 @@ def restore_asset(asset_name):
|
||||
|
||||
asset.set_status()
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_gl_entries_on_asset_regain(asset, selling_amount=0, finance_book=None):
|
||||
fixed_asset_account, asset, depreciation_cost_center, accumulated_depr_account, accumulated_depr_amount, disposal_account, value_after_depreciation = \
|
||||
get_asset_details(asset, finance_book)
|
||||
|
||||
gl_entries = [
|
||||
{
|
||||
"account": fixed_asset_account,
|
||||
"debit_in_account_currency": asset.gross_purchase_amount,
|
||||
"debit": asset.gross_purchase_amount,
|
||||
"cost_center": depreciation_cost_center
|
||||
},
|
||||
{
|
||||
"account": accumulated_depr_account,
|
||||
"credit_in_account_currency": accumulated_depr_amount,
|
||||
"credit": accumulated_depr_amount,
|
||||
"cost_center": depreciation_cost_center
|
||||
}
|
||||
]
|
||||
|
||||
profit_amount = abs(flt(value_after_depreciation)) - abs(flt(selling_amount))
|
||||
if profit_amount:
|
||||
get_profit_gl_entries(profit_amount, gl_entries, disposal_account, depreciation_cost_center)
|
||||
|
||||
return gl_entries
|
||||
|
||||
def get_gl_entries_on_asset_disposal(asset, selling_amount=0, finance_book=None):
|
||||
fixed_asset_account, accumulated_depr_account, depr_expense_account = get_depreciation_accounts(asset)
|
||||
disposal_account, depreciation_cost_center = get_disposal_account_and_cost_center(asset.company)
|
||||
depreciation_cost_center = asset.cost_center or depreciation_cost_center
|
||||
|
||||
idx = 1
|
||||
if finance_book:
|
||||
for d in asset.finance_books:
|
||||
if d.finance_book == finance_book:
|
||||
idx = d.idx
|
||||
break
|
||||
|
||||
value_after_depreciation = (asset.finance_books[idx - 1].value_after_depreciation
|
||||
if asset.calculate_depreciation else asset.value_after_depreciation)
|
||||
accumulated_depr_amount = flt(asset.gross_purchase_amount) - flt(value_after_depreciation)
|
||||
fixed_asset_account, asset, depreciation_cost_center, accumulated_depr_account, accumulated_depr_amount, disposal_account, value_after_depreciation = \
|
||||
get_asset_details(asset, finance_book)
|
||||
|
||||
gl_entries = [
|
||||
{
|
||||
@ -210,16 +222,37 @@ def get_gl_entries_on_asset_disposal(asset, selling_amount=0, finance_book=None)
|
||||
|
||||
profit_amount = flt(selling_amount) - flt(value_after_depreciation)
|
||||
if profit_amount:
|
||||
debit_or_credit = "debit" if profit_amount < 0 else "credit"
|
||||
gl_entries.append({
|
||||
"account": disposal_account,
|
||||
"cost_center": depreciation_cost_center,
|
||||
debit_or_credit: abs(profit_amount),
|
||||
debit_or_credit + "_in_account_currency": abs(profit_amount)
|
||||
})
|
||||
get_profit_gl_entries(profit_amount, gl_entries, disposal_account, depreciation_cost_center)
|
||||
|
||||
return gl_entries
|
||||
|
||||
def get_asset_details(asset, finance_book=None):
|
||||
fixed_asset_account, accumulated_depr_account, depr_expense_account = get_depreciation_accounts(asset)
|
||||
disposal_account, depreciation_cost_center = get_disposal_account_and_cost_center(asset.company)
|
||||
depreciation_cost_center = asset.cost_center or depreciation_cost_center
|
||||
|
||||
idx = 1
|
||||
if finance_book:
|
||||
for d in asset.finance_books:
|
||||
if d.finance_book == finance_book:
|
||||
idx = d.idx
|
||||
break
|
||||
|
||||
value_after_depreciation = (asset.finance_books[idx - 1].value_after_depreciation
|
||||
if asset.calculate_depreciation else asset.value_after_depreciation)
|
||||
accumulated_depr_amount = flt(asset.gross_purchase_amount) - flt(value_after_depreciation)
|
||||
|
||||
return fixed_asset_account, asset, depreciation_cost_center, accumulated_depr_account, accumulated_depr_amount, disposal_account, value_after_depreciation
|
||||
|
||||
def get_profit_gl_entries(profit_amount, gl_entries, disposal_account, depreciation_cost_center):
|
||||
debit_or_credit = "debit" if profit_amount < 0 else "credit"
|
||||
gl_entries.append({
|
||||
"account": disposal_account,
|
||||
"cost_center": depreciation_cost_center,
|
||||
debit_or_credit: abs(profit_amount),
|
||||
debit_or_credit + "_in_account_currency": abs(profit_amount)
|
||||
})
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_disposal_account_and_cost_center(company):
|
||||
disposal_account, depreciation_cost_center = frappe.get_cached_value('Company', company,
|
||||
|
@ -97,6 +97,9 @@
|
||||
"is_fixed_asset",
|
||||
"item_tax_rate",
|
||||
"section_break_72",
|
||||
"production_plan",
|
||||
"production_plan_item",
|
||||
"production_plan_sub_assembly_item",
|
||||
"page_break"
|
||||
],
|
||||
"fields": [
|
||||
@ -803,13 +806,37 @@
|
||||
"options": "Company:company:default_currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "production_plan",
|
||||
"fieldtype": "Link",
|
||||
"label": "Production Plan",
|
||||
"options": "Production Plan",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "production_plan_item",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Production Plan Item",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "production_plan_sub_assembly_item",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Production Plan Sub Assembly Item",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-03-22 11:46:12.357435",
|
||||
"modified": "2021-06-28 19:22:22.715365",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Order Item",
|
||||
|
69
erpnext/change_log/v13/v13_7_0.md
Normal file
69
erpnext/change_log/v13/v13_7_0.md
Normal file
@ -0,0 +1,69 @@
|
||||
# Version 13.7.0 Release Notes
|
||||
|
||||
### Features & Enhancements
|
||||
- Optionally allow rejected quality inspection on submission ([#26133](https://github.com/frappe/erpnext/pull/26133))
|
||||
- Bootstrapped GST Setup for India ([#25415](https://github.com/frappe/erpnext/pull/25415))
|
||||
- Fetching details from supplier/customer groups ([#26454](https://github.com/frappe/erpnext/pull/26454))
|
||||
- Provision to make subcontracted purchase order from the production plan ([#26240](https://github.com/frappe/erpnext/pull/26240))
|
||||
- Optimized code for reposting item valuation ([#26432](https://github.com/frappe/erpnext/pull/26432))
|
||||
|
||||
### Fixes
|
||||
- Auto process deferred accounting for multi-company setup ([#26277](https://github.com/frappe/erpnext/pull/26277))
|
||||
- Error while fetching item taxes ([#26218](https://github.com/frappe/erpnext/pull/26218))
|
||||
- Validation check for batch for stock reconciliation type in stock entry(bp #26370 ) ([#26488](https://github.com/frappe/erpnext/pull/26488))
|
||||
- Error popup for COA errors ([#26358](https://github.com/frappe/erpnext/pull/26358))
|
||||
- Precision for expected values in payment entry test ([#26394](https://github.com/frappe/erpnext/pull/26394))
|
||||
- Bank statement import ([#26287](https://github.com/frappe/erpnext/pull/26287))
|
||||
- LMS progress issue ([#26253](https://github.com/frappe/erpnext/pull/26253))
|
||||
- Paging buttons not working on item group portal page ([#26497](https://github.com/frappe/erpnext/pull/26497))
|
||||
- Omit item discount amount for e-invoicing ([#26353](https://github.com/frappe/erpnext/pull/26353))
|
||||
- Validate LCV for Invoices without Update Stock ([#26333](https://github.com/frappe/erpnext/pull/26333))
|
||||
- Remove cancelled entries in consolidated financial statements ([#26331](https://github.com/frappe/erpnext/pull/26331))
|
||||
- Fetching employee in payroll entry ([#26271](https://github.com/frappe/erpnext/pull/26271))
|
||||
- To fetch the correct field in Tax Rule ([#25927](https://github.com/frappe/erpnext/pull/25927))
|
||||
- Order and time of operations in multilevel BOM work order ([#25886](https://github.com/frappe/erpnext/pull/25886))
|
||||
- Fixed Budget Variance Graph color from all black to default ([#26368](https://github.com/frappe/erpnext/pull/26368))
|
||||
- TDS computation summary shows cancelled invoices (#26456) ([#26486](https://github.com/frappe/erpnext/pull/26486))
|
||||
- Do not consider cancelled entries in party dashboard ([#26231](https://github.com/frappe/erpnext/pull/26231))
|
||||
- Add validation for 'for_qty' else throws errors ([#25829](https://github.com/frappe/erpnext/pull/25829))
|
||||
- Move the rename abbreviation job to long queue (#26434) ([#26462](https://github.com/frappe/erpnext/pull/26462))
|
||||
- Query for Training Event ([#26388](https://github.com/frappe/erpnext/pull/26388))
|
||||
- Item group portal issues (backport) ([#26493](https://github.com/frappe/erpnext/pull/26493))
|
||||
- When lead is created with mobile_no, mobile_no value gets lost ([#26298](https://github.com/frappe/erpnext/pull/26298))
|
||||
- WIP needs to be set before submit on skip_transfer (bp #26499) ([#26507](https://github.com/frappe/erpnext/pull/26507))
|
||||
- Incorrect valuation rate in stock reconciliation ([#26259](https://github.com/frappe/erpnext/pull/26259))
|
||||
- Precision rate for packed items in internal transfers ([#26046](https://github.com/frappe/erpnext/pull/26046))
|
||||
- Changed profitability analysis report width ([#26165](https://github.com/frappe/erpnext/pull/26165))
|
||||
- Unable to download GSTR-1 json ([#26468](https://github.com/frappe/erpnext/pull/26468))
|
||||
- Unallocated amount in Payment Entry after taxes ([#26472](https://github.com/frappe/erpnext/pull/26472))
|
||||
- Include Stock Reco logic in `update_qty_in_future_sle` ([#26158](https://github.com/frappe/erpnext/pull/26158))
|
||||
- Update cost not working in the draft BOM ([#26279](https://github.com/frappe/erpnext/pull/26279))
|
||||
- Cancellation of Loan Security Pledges ([#26252](https://github.com/frappe/erpnext/pull/26252))
|
||||
- fix(e-invoicing): allow export invoice even if no taxes applied (#26363) ([#26405](https://github.com/frappe/erpnext/pull/26405))
|
||||
- Delete accounts (an empty file) ([#25323](https://github.com/frappe/erpnext/pull/25323))
|
||||
- Errors on parallel requests creation of company for India ([#26470](https://github.com/frappe/erpnext/pull/26470))
|
||||
- Incorrect bom no added for non-variant items on variant boms ([#26320](https://github.com/frappe/erpnext/pull/26320))
|
||||
- Incorrect discount amount on amended document ([#26466](https://github.com/frappe/erpnext/pull/26466))
|
||||
- Added a message to enable appointment booking if disabled ([#26334](https://github.com/frappe/erpnext/pull/26334))
|
||||
- fix(pos): taxes amount in pos item cart ([#26411](https://github.com/frappe/erpnext/pull/26411))
|
||||
- Track changes on batch ([#26382](https://github.com/frappe/erpnext/pull/26382))
|
||||
- Stock entry with putaway rule not working ([#26350](https://github.com/frappe/erpnext/pull/26350))
|
||||
- Only "Tax" type accounts should be shown for selection in GST Settings ([#26300](https://github.com/frappe/erpnext/pull/26300))
|
||||
- Added permission for employee to book appointment ([#26255](https://github.com/frappe/erpnext/pull/26255))
|
||||
- Allow to make job card without employee ([#26312](https://github.com/frappe/erpnext/pull/26312))
|
||||
- Project Portal Enhancements ([#26290](https://github.com/frappe/erpnext/pull/26290))
|
||||
- BOM stock report not working ([#26332](https://github.com/frappe/erpnext/pull/26332))
|
||||
- Order Items by weightage in the web items query ([#26284](https://github.com/frappe/erpnext/pull/26284))
|
||||
- Removed values out of sync validation from stock transactions ([#26226](https://github.com/frappe/erpnext/pull/26226))
|
||||
- Payroll-entry minor fix ([#26349](https://github.com/frappe/erpnext/pull/26349))
|
||||
- Allow user to change the To Date in the blanket order even after submit of order ([#26241](https://github.com/frappe/erpnext/pull/26241))
|
||||
- Value fetching for custom field in POS ([#26367](https://github.com/frappe/erpnext/pull/26367))
|
||||
- Iteration through accounts only when accounts exist ([#26391](https://github.com/frappe/erpnext/pull/26391))
|
||||
- Employee Inactive status implications ([#26244](https://github.com/frappe/erpnext/pull/26244))
|
||||
- Multi-currency issue ([#26458](https://github.com/frappe/erpnext/pull/26458))
|
||||
- FG item not fetched in manufacture entry ([#26509](https://github.com/frappe/erpnext/pull/26509))
|
||||
- Set query for training events ([#26303](https://github.com/frappe/erpnext/pull/26303))
|
||||
- Fetch batch items in stock reconciliation ([#26213](https://github.com/frappe/erpnext/pull/26213))
|
||||
- Employee selection not working in payroll entry ([#26278](https://github.com/frappe/erpnext/pull/26278))
|
||||
- POS item cart dom updates (#26459) ([#26461](https://github.com/frappe/erpnext/pull/26461))
|
||||
- dunning calculation of grand total when rate of interest is 0% ([#26285](https://github.com/frappe/erpnext/pull/26285))
|
@ -818,11 +818,11 @@ class AccountsController(TransactionBase):
|
||||
account_currency = get_account_currency(tax.account_head)
|
||||
|
||||
if self.doctype == "Purchase Invoice":
|
||||
dr_or_cr = "credit" if tax.add_deduct_tax == "Add" else "debit"
|
||||
rev_dr_cr = "debit" if tax.add_deduct_tax == "Add" else "credit"
|
||||
else:
|
||||
dr_or_cr = "debit" if tax.add_deduct_tax == "Add" else "credit"
|
||||
rev_dr_cr = "credit" if tax.add_deduct_tax == "Add" else "debit"
|
||||
else:
|
||||
dr_or_cr = "credit" if tax.add_deduct_tax == "Add" else "debit"
|
||||
rev_dr_cr = "debit" if tax.add_deduct_tax == "Add" else "credit"
|
||||
|
||||
party = self.supplier if self.doctype == "Purchase Invoice" else self.customer
|
||||
unallocated_amount = tax.tax_amount - tax.allocated_amount
|
||||
|
@ -102,7 +102,7 @@
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2021-06-30 12:09:14.228756",
|
||||
"modified": "2021-06-30 13:09:14.228756",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "Appointment",
|
||||
|
@ -7,16 +7,21 @@ import frappe
|
||||
import unittest
|
||||
from erpnext.erpnext_integrations.doctype.mpesa_settings.mpesa_settings import process_balance_info, verify_transaction
|
||||
from erpnext.accounts.doctype.pos_invoice.test_pos_invoice import create_pos_invoice
|
||||
from erpnext.erpnext_integrations.utils import create_mode_of_payment
|
||||
|
||||
class TestMpesaSettings(unittest.TestCase):
|
||||
def setUp(self):
|
||||
# create payment gateway in setup
|
||||
create_mpesa_settings(payment_gateway_name="_Test")
|
||||
create_mpesa_settings(payment_gateway_name="_Account Balance")
|
||||
create_mpesa_settings(payment_gateway_name="Payment")
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.sql('delete from `tabMpesa Settings`')
|
||||
frappe.db.sql('delete from `tabIntegration Request` where integration_request_service = "Mpesa"')
|
||||
|
||||
def test_creation_of_payment_gateway(self):
|
||||
create_mpesa_settings(payment_gateway_name="_Test")
|
||||
|
||||
mode_of_payment = frappe.get_doc("Mode of Payment", "Mpesa-_Test")
|
||||
mode_of_payment = create_mode_of_payment('Mpesa-_Test', payment_type="Phone")
|
||||
self.assertTrue(frappe.db.exists("Payment Gateway Account", {'payment_gateway': "Mpesa-_Test"}))
|
||||
self.assertTrue(mode_of_payment.name)
|
||||
self.assertEqual(mode_of_payment.type, "Phone")
|
||||
@ -47,7 +52,6 @@ class TestMpesaSettings(unittest.TestCase):
|
||||
integration_request.delete()
|
||||
|
||||
def test_processing_of_callback_payload(self):
|
||||
create_mpesa_settings(payment_gateway_name="Payment")
|
||||
mpesa_account = frappe.db.get_value("Payment Gateway Account", {"payment_gateway": 'Mpesa-Payment'}, "payment_account")
|
||||
frappe.db.set_value("Account", mpesa_account, "account_currency", "KES")
|
||||
frappe.db.set_value("Customer", "_Test Customer", "default_currency", "KES")
|
||||
@ -90,7 +94,6 @@ class TestMpesaSettings(unittest.TestCase):
|
||||
pos_invoice.delete()
|
||||
|
||||
def test_processing_of_multiple_callback_payload(self):
|
||||
create_mpesa_settings(payment_gateway_name="Payment")
|
||||
mpesa_account = frappe.db.get_value("Payment Gateway Account", {"payment_gateway": 'Mpesa-Payment'}, "payment_account")
|
||||
frappe.db.set_value("Account", mpesa_account, "account_currency", "KES")
|
||||
frappe.db.set_value("Mpesa Settings", "Payment", "transaction_limit", "500")
|
||||
@ -141,7 +144,6 @@ class TestMpesaSettings(unittest.TestCase):
|
||||
pos_invoice.delete()
|
||||
|
||||
def test_processing_of_only_one_succes_callback_payload(self):
|
||||
create_mpesa_settings(payment_gateway_name="Payment")
|
||||
mpesa_account = frappe.db.get_value("Payment Gateway Account", {"payment_gateway": 'Mpesa-Payment'}, "payment_account")
|
||||
frappe.db.set_value("Account", mpesa_account, "account_currency", "KES")
|
||||
frappe.db.set_value("Mpesa Settings", "Payment", "transaction_limit", "500")
|
||||
@ -202,6 +204,7 @@ def create_mpesa_settings(payment_gateway_name="Express"):
|
||||
|
||||
doc = frappe.get_doc(dict( #nosec
|
||||
doctype="Mpesa Settings",
|
||||
sandbox=1,
|
||||
payment_gateway_name=payment_gateway_name,
|
||||
consumer_key="5sMu9LVI1oS3oBGPJfh3JyvLHwZOdTKn",
|
||||
consumer_secret="VI1oS3oBGPJfh3JyvLHw",
|
||||
|
@ -52,7 +52,8 @@ def create_mode_of_payment(gateway, payment_type="General"):
|
||||
"payment_gateway": gateway
|
||||
}, ['payment_account'])
|
||||
|
||||
if not frappe.db.exists("Mode of Payment", gateway) and payment_gateway_account:
|
||||
mode_of_payment = frappe.db.exists("Mode of Payment", gateway)
|
||||
if not mode_of_payment and payment_gateway_account:
|
||||
mode_of_payment = frappe.get_doc({
|
||||
"doctype": "Mode of Payment",
|
||||
"mode_of_payment": gateway,
|
||||
@ -66,6 +67,10 @@ def create_mode_of_payment(gateway, payment_type="General"):
|
||||
})
|
||||
mode_of_payment.insert(ignore_permissions=True)
|
||||
|
||||
return mode_of_payment
|
||||
elif mode_of_payment:
|
||||
return frappe.get_doc("Mode of Payment", mode_of_payment)
|
||||
|
||||
def get_tracking_url(carrier, tracking_number):
|
||||
# Return the formatted Tracking URL.
|
||||
tracking_url = ''
|
||||
|
@ -245,7 +245,10 @@ doc_events = {
|
||||
"erpnext.portal.utils.set_default_role"]
|
||||
},
|
||||
"Communication": {
|
||||
"on_update": "erpnext.support.doctype.service_level_agreement.service_level_agreement.update_hold_time"
|
||||
"on_update": [
|
||||
"erpnext.support.doctype.service_level_agreement.service_level_agreement.update_hold_time",
|
||||
"erpnext.support.doctype.issue.issue.set_first_response_time"
|
||||
]
|
||||
},
|
||||
("Sales Taxes and Charges Template", 'Price List'): {
|
||||
"on_update": "erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings.validate_cart_settings"
|
||||
|
@ -72,7 +72,8 @@ class TestExpenseClaim(unittest.TestCase):
|
||||
def test_expense_claim_gl_entry(self):
|
||||
payable_account = get_payable_account(company_name)
|
||||
taxes = generate_taxes()
|
||||
expense_claim = make_expense_claim(payable_account, 300, 200, company_name, "Travel Expenses - _TC4", do_not_submit=True, taxes=taxes)
|
||||
expense_claim = make_expense_claim(payable_account, 300, 200, company_name, "Travel Expenses - _TC4",
|
||||
do_not_submit=True, taxes=taxes)
|
||||
expense_claim.submit()
|
||||
|
||||
gl_entries = frappe.db.sql("""select account, debit, credit
|
||||
@ -82,7 +83,7 @@ class TestExpenseClaim(unittest.TestCase):
|
||||
self.assertTrue(gl_entries)
|
||||
|
||||
expected_values = dict((d[0], d) for d in [
|
||||
['CGST - _TC4',18.0, 0.0],
|
||||
['Output Tax CGST - _TC4',18.0, 0.0],
|
||||
[payable_account, 0.0, 218.0],
|
||||
["Travel Expenses - _TC4", 200.0, 0.0]
|
||||
])
|
||||
@ -145,7 +146,7 @@ def generate_taxes():
|
||||
parent_account = frappe.db.get_value('Account',
|
||||
{'company': company_name, 'is_group':1, 'account_type': 'Tax'},
|
||||
'name')
|
||||
account = create_account(company=company_name, account_name="CGST", account_type="Tax", parent_account=parent_account)
|
||||
account = create_account(company=company_name, account_name="Output Tax CGST", account_type="Tax", parent_account=parent_account)
|
||||
return {'taxes':[{
|
||||
"account_head": account,
|
||||
"rate": 0,
|
||||
|
@ -35,7 +35,9 @@
|
||||
"no_copy": 1,
|
||||
"options": "Loan Security Pledge",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "loan_application.applicant",
|
||||
@ -45,47 +47,63 @@
|
||||
"in_standard_filter": 1,
|
||||
"label": "Applicant",
|
||||
"options": "applicant_type",
|
||||
"reqd": 1
|
||||
"reqd": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "loan_security_details_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Loan Security Details"
|
||||
"label": "Loan Security Details",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_3",
|
||||
"fieldtype": "Column Break"
|
||||
"fieldtype": "Column Break",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "loan",
|
||||
"fieldtype": "Link",
|
||||
"label": "Loan",
|
||||
"options": "Loan"
|
||||
"options": "Loan",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "loan_application",
|
||||
"fieldtype": "Link",
|
||||
"label": "Loan Application",
|
||||
"options": "Loan Application"
|
||||
"options": "Loan Application",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "total_security_value",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Total Security Value",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "maximum_loan_value",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Maximum Loan Value",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "loan_details_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Loan Details"
|
||||
"label": "Loan Details",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"default": "Requested",
|
||||
@ -94,37 +112,49 @@
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Status",
|
||||
"options": "Requested\nUnpledged\nPledged\nPartially Pledged",
|
||||
"read_only": 1
|
||||
"options": "Requested\nUnpledged\nPledged\nPartially Pledged\nCancelled",
|
||||
"read_only": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "pledge_time",
|
||||
"fieldtype": "Datetime",
|
||||
"label": "Pledge Time",
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "securities",
|
||||
"fieldtype": "Table",
|
||||
"label": "Securities",
|
||||
"options": "Pledge",
|
||||
"reqd": 1
|
||||
"reqd": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_11",
|
||||
"fieldtype": "Column Break"
|
||||
"fieldtype": "Column Break",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_10",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Totals"
|
||||
"label": "Totals",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"label": "Company",
|
||||
"options": "Company",
|
||||
"reqd": 1
|
||||
"reqd": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "loan.applicant_type",
|
||||
@ -132,35 +162,45 @@
|
||||
"fieldtype": "Select",
|
||||
"label": "Applicant Type",
|
||||
"options": "Employee\nMember\nCustomer",
|
||||
"reqd": 1
|
||||
"reqd": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "more_information_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "More Information"
|
||||
"label": "More Information",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "reference_no",
|
||||
"fieldtype": "Data",
|
||||
"label": "Reference No"
|
||||
"label": "Reference No",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_18",
|
||||
"fieldtype": "Column Break"
|
||||
"fieldtype": "Column Break",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Text",
|
||||
"label": "Description"
|
||||
"label": "Description",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-04-19 18:23:16.953305",
|
||||
"modified": "2021-06-29 17:15:16.082256",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Loan Management",
|
||||
"name": "Loan Security Pledge",
|
||||
|
@ -23,6 +23,12 @@ class LoanSecurityPledge(Document):
|
||||
update_shortfall_status(self.loan, self.total_security_value)
|
||||
update_loan(self.loan, self.maximum_loan_value)
|
||||
|
||||
def on_cancel(self):
|
||||
if self.loan:
|
||||
self.db_set("status", "Cancelled")
|
||||
self.db_set("pledge_time", None)
|
||||
update_loan(self.loan, self.maximum_loan_value, cancel=1)
|
||||
|
||||
def validate_duplicate_securities(self):
|
||||
security_list = []
|
||||
for security in self.securities:
|
||||
@ -36,7 +42,7 @@ class LoanSecurityPledge(Document):
|
||||
existing_pledge = ''
|
||||
|
||||
if self.loan:
|
||||
existing_pledge = frappe.db.get_value('Loan Security Pledge', {'loan': self.loan}, ['name'])
|
||||
existing_pledge = frappe.db.get_value('Loan Security Pledge', {'loan': self.loan, 'docstatus': 1}, ['name'])
|
||||
|
||||
if existing_pledge:
|
||||
loan_security_type = frappe.db.get_value('Pledge', {'parent': existing_pledge}, ['loan_security_type'])
|
||||
@ -77,8 +83,12 @@ class LoanSecurityPledge(Document):
|
||||
self.total_security_value = total_security_value
|
||||
self.maximum_loan_value = maximum_loan_value
|
||||
|
||||
def update_loan(loan, maximum_value_against_pledge):
|
||||
def update_loan(loan, maximum_value_against_pledge, cancel=0):
|
||||
maximum_loan_value = frappe.db.get_value('Loan', {'name': loan}, ['maximum_loan_amount'])
|
||||
|
||||
frappe.db.sql(""" UPDATE `tabLoan` SET maximum_loan_amount=%s, is_secured_loan=1
|
||||
WHERE name=%s""", (maximum_loan_value + maximum_value_against_pledge, loan))
|
||||
if cancel:
|
||||
frappe.db.sql(""" UPDATE `tabLoan` SET maximum_loan_amount=%s
|
||||
WHERE name=%s""", (maximum_loan_value - maximum_value_against_pledge, loan))
|
||||
else:
|
||||
frappe.db.sql(""" UPDATE `tabLoan` SET maximum_loan_amount=%s, is_secured_loan=1
|
||||
WHERE name=%s""", (maximum_loan_value + maximum_value_against_pledge, loan))
|
||||
|
@ -13,7 +13,7 @@ frappe.ui.form.on('Blanket Order', {
|
||||
|
||||
refresh: function(frm) {
|
||||
erpnext.hide_company();
|
||||
if (frm.doc.customer && frm.doc.docstatus === 1) {
|
||||
if (frm.doc.customer && frm.doc.docstatus === 1 && frm.doc.to_date > frappe.datetime.get_today()) {
|
||||
frm.add_custom_button(__("Sales Order"), function() {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.manufacturing.doctype.blanket_order.blanket_order.make_order",
|
||||
|
@ -1,4 +1,5 @@
|
||||
{
|
||||
"actions": [],
|
||||
"autoname": "naming_series:",
|
||||
"creation": "2018-05-24 07:18:08.256060",
|
||||
"doctype": "DocType",
|
||||
@ -79,6 +80,7 @@
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "to_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "To Date",
|
||||
@ -129,8 +131,10 @@
|
||||
"label": "Terms and Conditions Details"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"modified": "2019-11-18 19:37:37.151686",
|
||||
"links": [],
|
||||
"modified": "2021-06-29 00:30:30.621636",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Blanket Order",
|
||||
|
@ -36,6 +36,9 @@
|
||||
"materials_section",
|
||||
"inspection_required",
|
||||
"quality_inspection_template",
|
||||
"column_break_31",
|
||||
"bom_level",
|
||||
"section_break_33",
|
||||
"items",
|
||||
"scrap_section",
|
||||
"scrap_items",
|
||||
@ -513,6 +516,22 @@
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_31",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "bom_level",
|
||||
"fieldtype": "Int",
|
||||
"label": "BOM Level",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_33",
|
||||
"fieldtype": "Section Break",
|
||||
"hide_border": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-sitemap",
|
||||
@ -520,7 +539,7 @@
|
||||
"image_field": "image",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-03-16 12:25:09.081968",
|
||||
"modified": "2021-05-16 12:25:09.081968",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "BOM",
|
||||
|
@ -154,6 +154,7 @@ class BOM(WebsiteGenerator):
|
||||
self.calculate_cost()
|
||||
self.update_stock_qty()
|
||||
self.update_cost(update_parent=False, from_child_bom=True, update_hour_rate = False, save=False)
|
||||
self.set_bom_level()
|
||||
|
||||
def get_context(self, context):
|
||||
context.parents = [{'name': 'boms', 'title': _('All BOMs') }]
|
||||
@ -676,6 +677,19 @@ class BOM(WebsiteGenerator):
|
||||
"""Get a complete tree representation preserving order of child items."""
|
||||
return BOMTree(self.name)
|
||||
|
||||
def set_bom_level(self, update=False):
|
||||
levels = []
|
||||
|
||||
self.bom_level = 0
|
||||
for row in self.items:
|
||||
if row.bom_no:
|
||||
levels.append(frappe.get_cached_value("BOM", row.bom_no, "bom_level") or 0)
|
||||
|
||||
if levels:
|
||||
self.bom_level = max(levels) + 1
|
||||
|
||||
if update:
|
||||
self.db_set("bom_level", self.bom_level)
|
||||
|
||||
def get_bom_item_rate(args, bom_doc):
|
||||
if bom_doc.rm_cost_as_per == 'Valuation Rate':
|
||||
@ -699,7 +713,8 @@ def get_bom_item_rate(args, bom_doc):
|
||||
"conversion_rate": 1, # Passed conversion rate as 1 purposefully, as conversion rate is applied at the end of the function
|
||||
"conversion_factor": args.get("conversion_factor") or 1,
|
||||
"plc_conversion_rate": 1,
|
||||
"ignore_party": True
|
||||
"ignore_party": True,
|
||||
"ignore_conversion_rate": True
|
||||
})
|
||||
item_doc = frappe.get_cached_doc("Item", args.get("item_code"))
|
||||
out = frappe._dict()
|
||||
@ -860,7 +875,7 @@ def get_children(doctype, parent=None, is_root=False, **filters):
|
||||
frappe.form_dict.parent = parent
|
||||
|
||||
if frappe.form_dict.parent:
|
||||
bom_doc = frappe.get_doc("BOM", frappe.form_dict.parent)
|
||||
bom_doc = frappe.get_cached_doc("BOM", frappe.form_dict.parent)
|
||||
frappe.has_permission("BOM", doc=bom_doc, throw=True)
|
||||
|
||||
bom_items = frappe.get_all('BOM Item',
|
||||
@ -871,7 +886,7 @@ def get_children(doctype, parent=None, is_root=False, **filters):
|
||||
item_names = tuple(d.get('item_code') for d in bom_items)
|
||||
|
||||
items = frappe.get_list('Item',
|
||||
fields=['image', 'description', 'name', 'stock_uom', 'item_name'],
|
||||
fields=['image', 'description', 'name', 'stock_uom', 'item_name', 'is_sub_contracted_item'],
|
||||
filters=[['name', 'in', item_names]]) # to get only required item dicts
|
||||
|
||||
for bom_item in bom_items:
|
||||
@ -884,6 +899,7 @@ def get_children(doctype, parent=None, is_root=False, **filters):
|
||||
|
||||
bom_item.parent_bom_qty = bom_doc.quantity
|
||||
bom_item.expandable = 0 if bom_item.value in ('', None) else 1
|
||||
bom_item.image = frappe.db.escape(bom_item.image)
|
||||
|
||||
return bom_items
|
||||
|
||||
|
@ -1,13 +1,31 @@
|
||||
<div style="padding: 15px;">
|
||||
{% if data.image %}
|
||||
<img class="responsive" src={{ data.image }}>
|
||||
<hr style="margin: 15px -15px;">
|
||||
{% endif %}
|
||||
<h4>
|
||||
{{ __("Description") }}
|
||||
</h4>
|
||||
<div style="padding-top: 10px;">
|
||||
{{ data.description }}
|
||||
<div class="row mb-5">
|
||||
<div class="col-md-5" style="max-height: 500px">
|
||||
{% if data.image %}
|
||||
<div class="border image-field " style="overflow: hidden;border-color:#e6e6e6">
|
||||
<img class="responsive" src={{ data.image }}>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-7 h-500">
|
||||
<h4>
|
||||
{{ __("Description") }}
|
||||
</h4>
|
||||
<div style="padding-top: 10px;">
|
||||
{{ data.description }}
|
||||
</div>
|
||||
<hr style="margin: 15px -15px;">
|
||||
<p>
|
||||
{% if data.value %}
|
||||
<a style="margin-right: 7px; margin-bottom: 7px" class="btn btn-default btn-xs" href="#Form/BOM/{{ data.value }}">
|
||||
{{ __("Open BOM {0}", [data.value.bold()]) }}</a>
|
||||
{% endif %}
|
||||
{% if data.item_code %}
|
||||
<a class="btn btn-default btn-xs" href="#Form/Item/{{ data.item_code }}">
|
||||
{{ __("Open Item {0}", [data.item_code.bold()]) }}</a>
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<hr style="margin: 15px -15px;">
|
||||
<p>
|
||||
|
@ -64,7 +64,7 @@ frappe.treeview_settings["BOM"] = {
|
||||
if(node.is_root && node.data.value!="BOM") {
|
||||
frappe.model.with_doc("BOM", node.data.value, function() {
|
||||
var bom = frappe.model.get_doc("BOM", node.data.value);
|
||||
node.data.image = bom.image || "";
|
||||
node.data.image = escape(bom.image) || "";
|
||||
node.data.description = bom.description || "";
|
||||
});
|
||||
}
|
||||
|
@ -192,15 +192,20 @@ class JobCard(Document):
|
||||
"completed_qty": args.get("completed_qty") or 0.0
|
||||
})
|
||||
elif args.get("start_time"):
|
||||
for name in employees:
|
||||
self.append("time_logs", {
|
||||
"from_time": get_datetime(args.get("start_time")),
|
||||
"employee": name.get('employee'),
|
||||
"operation": args.get("sub_operation"),
|
||||
"completed_qty": 0.0
|
||||
})
|
||||
new_args = {
|
||||
"from_time": get_datetime(args.get("start_time")),
|
||||
"operation": args.get("sub_operation"),
|
||||
"completed_qty": 0.0
|
||||
}
|
||||
|
||||
if not self.employee:
|
||||
if employees:
|
||||
for name in employees:
|
||||
new_args.employee = name.get('employee')
|
||||
self.add_start_time_log(new_args)
|
||||
else:
|
||||
self.add_start_time_log(new_args)
|
||||
|
||||
if not self.employee and employees:
|
||||
self.set_employees(employees)
|
||||
|
||||
if self.status == "On Hold":
|
||||
@ -208,6 +213,9 @@ class JobCard(Document):
|
||||
|
||||
self.save()
|
||||
|
||||
def add_start_time_log(self, args):
|
||||
self.append("time_logs", args)
|
||||
|
||||
def set_employees(self, employees):
|
||||
for name in employees:
|
||||
self.append('employee', {
|
||||
|
@ -4,7 +4,7 @@
|
||||
frappe.ui.form.on('Production Plan', {
|
||||
setup: function(frm) {
|
||||
frm.custom_make_buttons = {
|
||||
'Work Order': 'Work Order',
|
||||
'Work Order': 'Work Order / Subcontract PO',
|
||||
'Material Request': 'Material Request',
|
||||
};
|
||||
|
||||
@ -68,17 +68,13 @@ frappe.ui.form.on('Production Plan', {
|
||||
frm.trigger("show_progress");
|
||||
|
||||
if (frm.doc.status !== "Completed") {
|
||||
if (frm.doc.po_items && frm.doc.status !== "Closed") {
|
||||
frm.add_custom_button(__("Work Order"), ()=> {
|
||||
frm.trigger("make_work_order");
|
||||
}, __('Create'));
|
||||
}
|
||||
frm.add_custom_button(__("Work Order Tree"), ()=> {
|
||||
frappe.set_route('Tree', 'Work Order', {production_plan: frm.doc.name});
|
||||
}, __('View'));
|
||||
|
||||
if (frm.doc.mr_items && !in_list(['Material Requested', 'Closed'], frm.doc.status)) {
|
||||
frm.add_custom_button(__("Material Request"), ()=> {
|
||||
frm.trigger("make_material_request");
|
||||
}, __('Create'));
|
||||
}
|
||||
frm.add_custom_button(__("Production Plan Summary"), ()=> {
|
||||
frappe.set_route('query-report', 'Production Plan Summary', {production_plan: frm.doc.name});
|
||||
}, __('View'));
|
||||
|
||||
if (frm.doc.status === "Closed") {
|
||||
frm.add_custom_button(__("Re-open"), function() {
|
||||
@ -89,6 +85,18 @@ frappe.ui.form.on('Production Plan', {
|
||||
frm.events.close_open_production_plan(frm, true);
|
||||
}, __("Status"));
|
||||
}
|
||||
|
||||
if (frm.doc.po_items && frm.doc.status !== "Closed") {
|
||||
frm.add_custom_button(__("Work Order / Subcontract PO"), ()=> {
|
||||
frm.trigger("make_work_order");
|
||||
}, __('Create'));
|
||||
}
|
||||
|
||||
if (frm.doc.mr_items && !in_list(['Material Requested', 'Closed'], frm.doc.status)) {
|
||||
frm.add_custom_button(__("Material Request"), ()=> {
|
||||
frm.trigger("make_material_request");
|
||||
}, __('Create'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -233,6 +241,17 @@ frappe.ui.form.on('Production Plan', {
|
||||
});
|
||||
},
|
||||
|
||||
get_sub_assembly_items: function(frm) {
|
||||
frappe.call({
|
||||
method: "get_sub_assembly_items",
|
||||
freeze: true,
|
||||
doc: frm.doc,
|
||||
callback: function() {
|
||||
refresh_field("sub_assembly_items");
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
get_items_for_mr: function(frm) {
|
||||
if (!frm.doc.for_warehouse) {
|
||||
frappe.throw(__("Select warehouse for material requests"));
|
||||
|
@ -32,6 +32,9 @@
|
||||
"po_items",
|
||||
"section_break_25",
|
||||
"prod_plan_references",
|
||||
"section_break_24",
|
||||
"get_sub_assembly_items",
|
||||
"sub_assembly_items",
|
||||
"material_request_planning",
|
||||
"include_non_stock_items",
|
||||
"include_subcontracted_items",
|
||||
@ -187,7 +190,7 @@
|
||||
"depends_on": "get_items_from",
|
||||
"fieldname": "get_items",
|
||||
"fieldtype": "Button",
|
||||
"label": "Get Items For Work Order"
|
||||
"label": "Get Finished Goods for Manufacture"
|
||||
},
|
||||
{
|
||||
"fieldname": "po_items",
|
||||
@ -199,7 +202,7 @@
|
||||
{
|
||||
"fieldname": "material_request_planning",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Material Request Planning"
|
||||
"label": "Material Requirement Planning"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
@ -237,12 +240,13 @@
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_27",
|
||||
"fieldtype": "Section Break"
|
||||
"fieldtype": "Section Break",
|
||||
"hide_border": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "mr_items",
|
||||
"fieldtype": "Table",
|
||||
"label": "Material Request Plan Item",
|
||||
"label": "Raw Materials",
|
||||
"no_copy": 1,
|
||||
"options": "Material Request Plan Item"
|
||||
},
|
||||
@ -337,13 +341,30 @@
|
||||
"hidden": 1,
|
||||
"label": "Production Plan Item Reference",
|
||||
"options": "Production Plan Item Reference"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_24",
|
||||
"fieldtype": "Section Break",
|
||||
"hide_border": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "sub_assembly_items",
|
||||
"fieldtype": "Table",
|
||||
"label": "Sub Assembly Items",
|
||||
"no_copy": 1,
|
||||
"options": "Production Plan Sub Assembly Item"
|
||||
},
|
||||
{
|
||||
"fieldname": "get_sub_assembly_items",
|
||||
"fieldtype": "Button",
|
||||
"label": "Get Sub Assembly Items"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-calendar",
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-05-24 16:59:03.643211",
|
||||
"modified": "2021-06-28 20:00:33.905114",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Production Plan",
|
||||
|
@ -5,10 +5,11 @@
|
||||
from __future__ import unicode_literals
|
||||
import frappe, json, copy
|
||||
from frappe import msgprint, _
|
||||
from six import string_types, iteritems
|
||||
from six import iteritems
|
||||
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import cstr, flt, cint, nowdate, add_days, comma_and, now_datetime, ceil
|
||||
from frappe.utils import (flt, cint, nowdate, add_days, comma_and, now_datetime,
|
||||
ceil, get_link_to_form, getdate)
|
||||
from frappe.utils.csvutils import build_csv_response
|
||||
from erpnext.manufacturing.doctype.bom.bom import validate_bom_no, get_children
|
||||
from erpnext.manufacturing.doctype.work_order.work_order import get_item_details
|
||||
@ -349,49 +350,88 @@ class ProductionPlan(Document):
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_work_order(self):
|
||||
wo_list = []
|
||||
wo_list, po_list = [], []
|
||||
subcontracted_po = {}
|
||||
|
||||
self.validate_data()
|
||||
self.make_work_order_for_finished_goods(wo_list)
|
||||
self.make_work_order_for_subassembly_items(wo_list, subcontracted_po)
|
||||
self.make_subcontracted_purchase_order(subcontracted_po, po_list)
|
||||
self.show_list_created_message('Work Order', wo_list)
|
||||
self.show_list_created_message('Purchase Order', po_list)
|
||||
|
||||
def make_work_order_for_finished_goods(self, wo_list):
|
||||
items_data = self.get_production_items()
|
||||
|
||||
for key, item in items_data.items():
|
||||
if self.sub_assembly_items:
|
||||
item['use_multi_level_bom'] = 0
|
||||
|
||||
work_order = self.create_work_order(item)
|
||||
if work_order:
|
||||
wo_list.append(work_order)
|
||||
|
||||
if item.get("make_work_order_for_sub_assembly_items"):
|
||||
work_orders = self.make_work_order_for_sub_assembly_items(item)
|
||||
wo_list.extend(work_orders)
|
||||
def make_work_order_for_subassembly_items(self, wo_list, subcontracted_po):
|
||||
for row in self.sub_assembly_items:
|
||||
if row.type_of_manufacturing == 'Subcontract':
|
||||
subcontracted_po.setdefault(row.supplier, []).append(row)
|
||||
continue
|
||||
|
||||
args = {}
|
||||
self.prepare_args_for_sub_assembly_items(row, args)
|
||||
work_order = self.create_work_order(args)
|
||||
if work_order:
|
||||
wo_list.append(work_order)
|
||||
|
||||
def make_subcontracted_purchase_order(self, subcontracted_po, purchase_orders):
|
||||
if not subcontracted_po:
|
||||
return
|
||||
|
||||
for supplier, po_list in subcontracted_po.items():
|
||||
po = frappe.new_doc('Purchase Order')
|
||||
po.supplier = supplier
|
||||
po.schedule_date = getdate(po_list[0].schedule_date) if po_list[0].schedule_date else nowdate()
|
||||
po.is_subcontracted_item = 'Yes'
|
||||
for row in po_list:
|
||||
args = {
|
||||
'item_code': row.production_item,
|
||||
'warehouse': row.fg_warehouse,
|
||||
'production_plan_sub_assembly_item': row.name,
|
||||
'bom': row.bom_no,
|
||||
'production_plan': self.name
|
||||
}
|
||||
|
||||
for field in ['schedule_date', 'qty', 'uom', 'stock_uom', 'item_name',
|
||||
'description', 'production_plan_item']:
|
||||
args[field] = row.get(field)
|
||||
|
||||
po.append('items', args)
|
||||
|
||||
po.set_missing_values()
|
||||
po.flags.ignore_mandatory = True
|
||||
po.flags.ignore_validate = True
|
||||
po.insert()
|
||||
purchase_orders.append(po.name)
|
||||
|
||||
def show_list_created_message(self, doctype, doc_list=None):
|
||||
if not doc_list:
|
||||
return
|
||||
|
||||
frappe.flags.mute_messages = False
|
||||
if doc_list:
|
||||
doc_list = [get_link_to_form(doctype, p) for p in doc_list]
|
||||
msgprint(_("{0} created").format(comma_and(doc_list)))
|
||||
|
||||
if wo_list:
|
||||
wo_list = ["""<a href="/app/Form/Work Order/%s" target="_blank">%s</a>""" % \
|
||||
(p, p) for p in wo_list]
|
||||
msgprint(_("{0} created").format(comma_and(wo_list)))
|
||||
else :
|
||||
msgprint(_("No Work Orders created"))
|
||||
def prepare_args_for_sub_assembly_items(self, row, args):
|
||||
for field in ["production_item", "item_name", "qty", "fg_warehouse",
|
||||
"description", "bom_no", "stock_uom", "bom_level", "production_plan_item"]:
|
||||
args[field] = row.get(field)
|
||||
|
||||
def make_work_order_for_sub_assembly_items(self, item):
|
||||
work_orders = []
|
||||
bom_data = {}
|
||||
|
||||
get_sub_assembly_items(item.get("bom_no"), bom_data, item.get("qty"))
|
||||
|
||||
for key, data in bom_data.items():
|
||||
data.update({
|
||||
'qty': data.get("stock_qty"),
|
||||
'production_plan': self.name,
|
||||
'use_multi_level_bom': item.get("use_multi_level_bom"),
|
||||
'company': self.company,
|
||||
'fg_warehouse': item.get("fg_warehouse"),
|
||||
'update_consumed_material_cost_in_project': 0
|
||||
})
|
||||
|
||||
work_order = self.create_work_order(data)
|
||||
if work_order:
|
||||
work_orders.append(work_order)
|
||||
|
||||
return work_orders
|
||||
args.update({
|
||||
"use_multi_level_bom": 0,
|
||||
"production_plan": self.name,
|
||||
"production_plan_sub_assembly_item": row.name
|
||||
})
|
||||
|
||||
def create_work_order(self, item):
|
||||
from erpnext.manufacturing.doctype.work_order.work_order import OverProductionError, get_default_warehouse
|
||||
@ -476,9 +516,32 @@ class ProductionPlan(Document):
|
||||
else :
|
||||
msgprint(_("No material request created"))
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_sub_assembly_items(self, manufacturing_type=None):
|
||||
self.sub_assembly_items = []
|
||||
for row in self.po_items:
|
||||
bom_data = []
|
||||
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)
|
||||
|
||||
for data in bom_data:
|
||||
data.qty = data.stock_qty
|
||||
data.production_plan_item = row.name
|
||||
data.fg_warehouse = row.warehouse
|
||||
data.schedule_date = row.planned_start_date
|
||||
data.type_of_manufacturing = manufacturing_type or ("Subcontract" if data.is_sub_contracted_item
|
||||
else "In House")
|
||||
|
||||
self.append("sub_assembly_items", data)
|
||||
|
||||
@frappe.whitelist()
|
||||
def download_raw_materials(doc, warehouses=None):
|
||||
if isinstance(doc, string_types):
|
||||
if isinstance(doc, str):
|
||||
doc = frappe._dict(json.loads(doc))
|
||||
|
||||
item_list = [['Item Code', 'Description', 'Stock UOM', 'Warehouse', 'Required Qty as per BOM',
|
||||
@ -660,7 +723,7 @@ def get_sales_orders(self):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_bin_details(row, company, for_warehouse=None, all_warehouse=False):
|
||||
if isinstance(row, string_types):
|
||||
if isinstance(row, str):
|
||||
row = frappe._dict(json.loads(row))
|
||||
|
||||
company = frappe.db.escape(company)
|
||||
@ -684,8 +747,11 @@ 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)
|
||||
|
||||
def get_warehouse_list(warehouses, warehouse_list=[]):
|
||||
if isinstance(warehouses, string_types):
|
||||
def get_warehouse_list(warehouses, warehouse_list=None):
|
||||
if not warehouse_list:
|
||||
warehouse_list = []
|
||||
|
||||
if isinstance(warehouses, str):
|
||||
warehouses = json.loads(warehouses)
|
||||
|
||||
for row in warehouses:
|
||||
@ -697,7 +763,7 @@ def get_warehouse_list(warehouses, warehouse_list=[]):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_data=None):
|
||||
if isinstance(doc, string_types):
|
||||
if isinstance(doc, str):
|
||||
doc = frappe._dict(json.loads(doc))
|
||||
|
||||
warehouse_list = []
|
||||
@ -726,6 +792,9 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d
|
||||
|
||||
so_item_details = frappe._dict()
|
||||
for data in po_items:
|
||||
if not data.get("include_exploded_items") and doc.get("sub_assembly_items"):
|
||||
data["include_exploded_items"] = 1
|
||||
|
||||
planned_qty = data.get('required_qty') or data.get('planned_qty')
|
||||
ignore_existing_ordered_qty = data.get('ignore_existing_ordered_qty') or ignore_existing_ordered_qty
|
||||
warehouse = doc.get('for_warehouse')
|
||||
@ -857,23 +926,28 @@ def get_item_data(item_code):
|
||||
# "description": item_details.get("description")
|
||||
}
|
||||
|
||||
def get_sub_assembly_items(bom_no, bom_data, to_produce_qty):
|
||||
def get_sub_assembly_items(bom_no, bom_data, to_produce_qty, indent=0):
|
||||
data = get_children('BOM', parent = bom_no)
|
||||
for d in data:
|
||||
if d.expandable:
|
||||
key = (d.name, d.value)
|
||||
if key not in bom_data:
|
||||
bom_data.setdefault(key, {
|
||||
'stock_qty': 0,
|
||||
'description': d.description,
|
||||
'production_item': d.item_code,
|
||||
'item_name': d.item_name,
|
||||
'stock_uom': d.stock_uom,
|
||||
'uom': d.stock_uom,
|
||||
'bom_no': d.value
|
||||
})
|
||||
parent_item_code = frappe.get_cached_value("BOM", bom_no, "item")
|
||||
bom_level = (frappe.get_cached_value("BOM", d.value, "bom_level")
|
||||
if d.value else 0)
|
||||
|
||||
bom_item = bom_data.get(key)
|
||||
bom_item["stock_qty"] += (d.stock_qty / d.parent_bom_qty) * flt(to_produce_qty)
|
||||
stock_qty = (d.stock_qty / d.parent_bom_qty) * flt(to_produce_qty)
|
||||
bom_data.append(frappe._dict({
|
||||
'parent_item_code': parent_item_code,
|
||||
'description': d.description,
|
||||
'production_item': d.item_code,
|
||||
'item_name': d.item_name,
|
||||
'stock_uom': d.stock_uom,
|
||||
'uom': d.stock_uom,
|
||||
'bom_no': d.value,
|
||||
'is_sub_contracted_item': d.is_sub_contracted_item,
|
||||
'bom_level': bom_level,
|
||||
'indent': indent,
|
||||
'stock_qty': stock_qty
|
||||
}))
|
||||
|
||||
get_sub_assembly_items(bom_item.get("bom_no"), bom_data, bom_item["stock_qty"])
|
||||
if d.value:
|
||||
get_sub_assembly_items(d.value, bom_data, stock_qty, indent=indent+1)
|
||||
|
@ -9,5 +9,9 @@ def get_data():
|
||||
'label': _('Transactions'),
|
||||
'items': ['Work Order', 'Material Request']
|
||||
},
|
||||
{
|
||||
'label': _('Subcontract'),
|
||||
'items': ['Purchase Order']
|
||||
},
|
||||
]
|
||||
}
|
@ -236,10 +236,10 @@ class TestProductionPlan(unittest.TestCase):
|
||||
pln.append("po_items", {
|
||||
"item_code": item_code,
|
||||
"bom_no": frappe.db.get_value('BOM', {'item': "Test BOM 1"}),
|
||||
"planned_qty": 3,
|
||||
"make_work_order_for_sub_assembly_items": 1
|
||||
"planned_qty": 3
|
||||
})
|
||||
|
||||
pln.get_sub_assembly_items('In House')
|
||||
pln.submit()
|
||||
pln.make_work_order()
|
||||
|
||||
|
@ -9,18 +9,17 @@
|
||||
"include_exploded_items",
|
||||
"item_code",
|
||||
"bom_no",
|
||||
"planned_qty",
|
||||
"column_break_6",
|
||||
"make_work_order_for_sub_assembly_items",
|
||||
"planned_qty",
|
||||
"warehouse",
|
||||
"planned_start_date",
|
||||
"section_break_9",
|
||||
"pending_qty",
|
||||
"ordered_qty",
|
||||
"produced_qty",
|
||||
"column_break_17",
|
||||
"description",
|
||||
"stock_uom",
|
||||
"produced_qty",
|
||||
"reference_section",
|
||||
"sales_order",
|
||||
"sales_order_item",
|
||||
@ -32,11 +31,10 @@
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"columns": 2,
|
||||
"default": "0",
|
||||
"columns": 1,
|
||||
"default": "1",
|
||||
"fieldname": "include_exploded_items",
|
||||
"fieldtype": "Check",
|
||||
"in_list_view": 1,
|
||||
"label": "Include Exploded Items"
|
||||
},
|
||||
{
|
||||
@ -80,13 +78,6 @@
|
||||
"fieldname": "column_break_6",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "If enabled, system will create the work order for the exploded items against which BOM is available.",
|
||||
"fieldname": "make_work_order_for_sub_assembly_items",
|
||||
"fieldtype": "Check",
|
||||
"label": "Make Work Order for Sub Assembly Items"
|
||||
},
|
||||
{
|
||||
"fieldname": "warehouse",
|
||||
"fieldtype": "Link",
|
||||
@ -218,7 +209,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-04-28 19:14:57.772123",
|
||||
"modified": "2021-06-28 18:31:06.822168",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Production Plan Item",
|
||||
|
@ -0,0 +1,202 @@
|
||||
{
|
||||
"actions": [],
|
||||
"creation": "2020-12-27 16:08:36.127199",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"production_item",
|
||||
"item_name",
|
||||
"fg_warehouse",
|
||||
"parent_item_code",
|
||||
"schedule_date",
|
||||
"column_break_3",
|
||||
"qty",
|
||||
"bom_no",
|
||||
"bom_level",
|
||||
"type_of_manufacturing",
|
||||
"supplier",
|
||||
"work_order_details_section",
|
||||
"work_order",
|
||||
"purchase_order",
|
||||
"production_plan_item",
|
||||
"column_break_7",
|
||||
"produced_qty",
|
||||
"received_qty",
|
||||
"indent",
|
||||
"section_break_19",
|
||||
"uom",
|
||||
"stock_uom",
|
||||
"column_break_22",
|
||||
"description"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fetch_from": "sub_assembly_item_code.item_name",
|
||||
"fieldname": "item_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "Item Name",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_3",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.type_of_manufacturing == \"In House\"",
|
||||
"fieldname": "work_order_details_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Reference"
|
||||
},
|
||||
{
|
||||
"fieldname": "work_order",
|
||||
"fieldtype": "Link",
|
||||
"label": "Work Order",
|
||||
"options": "Work Order",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_7",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"columns": 1,
|
||||
"fieldname": "qty",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Required Qty",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "purchase_order",
|
||||
"fieldtype": "Link",
|
||||
"label": "Purchase Order",
|
||||
"options": "Purchase Order",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "received_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Received Qty"
|
||||
},
|
||||
{
|
||||
"fieldname": "bom_no",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Bom No",
|
||||
"options": "BOM"
|
||||
},
|
||||
{
|
||||
"fieldname": "production_plan_item",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Production Plan Item",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "parent_item_code",
|
||||
"fieldtype": "Link",
|
||||
"label": "Finished Good",
|
||||
"options": "Item",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"columns": 1,
|
||||
"fetch_from": "bom_no.bom_level",
|
||||
"fieldname": "bom_level",
|
||||
"fieldtype": "Int",
|
||||
"in_list_view": 1,
|
||||
"label": "Level (BOM)",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "section_break_19",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Item Details"
|
||||
},
|
||||
{
|
||||
"fieldname": "uom",
|
||||
"fieldtype": "Link",
|
||||
"label": "UOM",
|
||||
"options": "UOM",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "stock_uom",
|
||||
"fieldtype": "Link",
|
||||
"label": "Stock UOM",
|
||||
"options": "UOM",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_22",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "description",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "production_item",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Sub Assembly Item Code",
|
||||
"options": "Item",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "indent",
|
||||
"fieldtype": "Int",
|
||||
"label": "Indent"
|
||||
},
|
||||
{
|
||||
"fieldname": "fg_warehouse",
|
||||
"fieldtype": "Link",
|
||||
"label": "Target Warehouse",
|
||||
"options": "Warehouse"
|
||||
},
|
||||
{
|
||||
"fieldname": "produced_qty",
|
||||
"fieldtype": "Data",
|
||||
"label": "Produced Quantity",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "In House",
|
||||
"fieldname": "type_of_manufacturing",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Manufacturing Type",
|
||||
"options": "In House\nSubcontract"
|
||||
},
|
||||
{
|
||||
"fieldname": "supplier",
|
||||
"fieldtype": "Link",
|
||||
"label": "Supplier",
|
||||
"mandatory_depends_on": "eval:doc.type_of_manufacturing == 'Subcontract'",
|
||||
"options": "Supplier"
|
||||
},
|
||||
{
|
||||
"fieldname": "schedule_date",
|
||||
"fieldtype": "Datetime",
|
||||
"in_list_view": 1,
|
||||
"label": "Schedule Date"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-06-28 20:10:56.296410",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Production Plan Sub Assembly Item",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
class ProductionPlanSubAssemblyItem(Document):
|
||||
pass
|
@ -19,6 +19,7 @@
|
||||
"options": "Operation"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Time in mins",
|
||||
"fieldname": "time_in_mins",
|
||||
"fieldtype": "Float",
|
||||
@ -38,7 +39,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-12-07 18:09:18.005578",
|
||||
"modified": "2021-07-15 16:39:41.635362",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Sub Operation",
|
||||
|
@ -513,6 +513,60 @@ class TestWorkOrder(unittest.TestCase):
|
||||
work_order1.save()
|
||||
self.assertEqual(work_order1.operations[0].time_in_mins, 40.0)
|
||||
|
||||
def test_batch_size_for_fg_item(self):
|
||||
fg_item = "Test Batch Size Item For BOM 3"
|
||||
rm1 = "Test Batch Size Item RM 1 For BOM 3"
|
||||
|
||||
frappe.db.set_value('Manufacturing Settings', None, 'make_serial_no_batch_from_work_order', 0)
|
||||
for item in ["Test Batch Size Item For BOM 3", "Test Batch Size Item RM 1 For BOM 3"]:
|
||||
item_args = {
|
||||
"include_item_in_manufacturing": 1,
|
||||
"is_stock_item": 1
|
||||
}
|
||||
|
||||
if item == fg_item:
|
||||
item_args['has_batch_no'] = 1
|
||||
item_args['create_new_batch'] = 1
|
||||
item_args['batch_number_series'] = 'TBSI3.#####'
|
||||
|
||||
make_item(item, item_args)
|
||||
|
||||
bom_name = frappe.db.get_value("BOM",
|
||||
{"item": fg_item, "is_active": 1, "with_operations": 1}, "name")
|
||||
|
||||
if not bom_name:
|
||||
bom = make_bom(item=fg_item, rate=1000, raw_materials = [rm1], do_not_save=True)
|
||||
bom.save()
|
||||
bom.submit()
|
||||
bom_name = bom.name
|
||||
|
||||
work_order = make_wo_order_test_record(item=fg_item, skip_transfer=True, planned_start_date=now(), qty=1)
|
||||
ste1 = frappe.get_doc(make_stock_entry(work_order.name, "Manufacture", 1))
|
||||
for row in ste1.get('items'):
|
||||
if row.is_finished_item:
|
||||
self.assertEqual(row.item_code, fg_item)
|
||||
|
||||
work_order = make_wo_order_test_record(item=fg_item, skip_transfer=True, planned_start_date=now(), qty=1)
|
||||
frappe.db.set_value('Manufacturing Settings', None, 'make_serial_no_batch_from_work_order', 1)
|
||||
ste1 = frappe.get_doc(make_stock_entry(work_order.name, "Manufacture", 1))
|
||||
for row in ste1.get('items'):
|
||||
if row.is_finished_item:
|
||||
self.assertEqual(row.item_code, fg_item)
|
||||
|
||||
work_order = make_wo_order_test_record(item=fg_item, skip_transfer=True, planned_start_date=now(),
|
||||
qty=30, do_not_save = True)
|
||||
work_order.batch_size = 10
|
||||
work_order.insert()
|
||||
work_order.submit()
|
||||
self.assertEqual(work_order.has_batch_no, 1)
|
||||
ste1 = frappe.get_doc(make_stock_entry(work_order.name, "Manufacture", 30))
|
||||
for row in ste1.get('items'):
|
||||
if row.is_finished_item:
|
||||
self.assertEqual(row.item_code, fg_item)
|
||||
self.assertEqual(row.qty, 10)
|
||||
|
||||
frappe.db.set_value('Manufacturing Settings', None, 'make_serial_no_batch_from_work_order', 0)
|
||||
|
||||
def test_partial_material_consumption(self):
|
||||
frappe.db.set_value("Manufacturing Settings", None, "material_consumption", 1)
|
||||
wo_order = make_wo_order_test_record(planned_start_date=now(), qty=4)
|
||||
|
@ -64,11 +64,16 @@
|
||||
"description",
|
||||
"stock_uom",
|
||||
"column_break2",
|
||||
"references_section",
|
||||
"material_request",
|
||||
"material_request_item",
|
||||
"sales_order_item",
|
||||
"column_break_61",
|
||||
"production_plan",
|
||||
"production_plan_item",
|
||||
"production_plan_sub_assembly_item",
|
||||
"parent_work_order",
|
||||
"bom_level",
|
||||
"product_bundle_item",
|
||||
"amended_from"
|
||||
],
|
||||
@ -546,17 +551,26 @@
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"fieldname": "production_plan_sub_assembly_item",
|
||||
"fieldtype": "Data",
|
||||
"label": "Production Plan Sub-assembly Item",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-cogs",
|
||||
"idx": 1,
|
||||
"image_field": "image",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-06-20 15:19:14.902699",
|
||||
"modified": "2021-06-28 16:19:14.902699",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Work Order",
|
||||
"nsm_parent_field": "parent_work_order",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
|
@ -239,7 +239,7 @@ class WorkOrder(Document):
|
||||
self.create_serial_no_batch_no()
|
||||
|
||||
def on_submit(self):
|
||||
if not self.wip_warehouse:
|
||||
if not self.wip_warehouse and not self.skip_transfer:
|
||||
frappe.throw(_("Work-in-Progress Warehouse is required before Submit"))
|
||||
if not self.fg_warehouse:
|
||||
frappe.throw(_("For Warehouse is required before Submit"))
|
||||
@ -483,7 +483,7 @@ class WorkOrder(Document):
|
||||
|
||||
|
||||
self.set('operations', [])
|
||||
if not self.bom_no:
|
||||
if not self.bom_no or not frappe.get_cached_value('BOM', self.bom_no, 'with_operations'):
|
||||
return
|
||||
|
||||
operations = []
|
||||
@ -590,6 +590,7 @@ class WorkOrder(Document):
|
||||
def validate_operation_time(self):
|
||||
for d in self.operations:
|
||||
if not d.time_in_mins > 0:
|
||||
print(self.bom_no, self.production_item)
|
||||
frappe.throw(_("Operation Time must be greater than 0 for Operation {0}").format(d.operation))
|
||||
|
||||
def update_required_items(self):
|
||||
|
@ -0,0 +1,33 @@
|
||||
<div style="padding: 15px;">
|
||||
<div class="row mb-5">
|
||||
<div class="col-md-5" style="max-height: 500px">
|
||||
{% if data.image %}
|
||||
<div class="border image-field " style="overflow: hidden;border-color:#e6e6e6">
|
||||
<img class="responsive" src={{ data.image }}>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-7 h-500">
|
||||
<div style="padding-top: 10px;">
|
||||
<b> Status </b> {{ data.status }}
|
||||
</div>
|
||||
<div style="padding-top: 10px;">
|
||||
<b> Qty to Produce </b> {{ data.qty }}
|
||||
</div>
|
||||
<div style="padding-top: 10px;">
|
||||
<b> Produced Qty </b> {{ data.produced_qty }}
|
||||
</div>
|
||||
<hr style="margin: 15px -15px;">
|
||||
<p>
|
||||
{% if data.value %}
|
||||
<a style="margin-right: 7px; margin-bottom: 7px" class="btn btn-default btn-xs" href="#Form/Work Order/{{ data.value }}">
|
||||
{{ __("Open Work Order {0}", [data.value.bold()]) }}</a>
|
||||
{% endif %}
|
||||
{% if data.item_code %}
|
||||
<a class="btn btn-default btn-xs" href="#Form/Item/{{ data.item_code }}">
|
||||
{{ __("Open Item {0}", [data.item_code.bold()]) }}</a>
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -20,17 +20,20 @@ def get_exploded_items(bom, data, indent=0, qty=1):
|
||||
fields= ['qty','bom_no','qty','scrap','item_code','item_name','description','uom'])
|
||||
|
||||
for item in exploded_items:
|
||||
print(item.bom_no, indent)
|
||||
item["indent"] = indent
|
||||
data.append({
|
||||
'item_code': item.item_code,
|
||||
'item_name': item.item_name,
|
||||
'indent': indent,
|
||||
'bom_level': (frappe.get_cached_value("BOM", item.bom_no, "bom_level")
|
||||
if item.bom_no else ""),
|
||||
'bom': item.bom_no,
|
||||
'qty': item.qty * qty,
|
||||
'uom': item.uom,
|
||||
'description': item.description,
|
||||
'scrap': item.scrap
|
||||
})
|
||||
})
|
||||
if item.bom_no:
|
||||
get_exploded_items(item.bom_no, data, indent=indent+1, qty=item.qty)
|
||||
|
||||
@ -68,6 +71,12 @@ def get_columns():
|
||||
"fieldname": "uom",
|
||||
"width": 100
|
||||
},
|
||||
{
|
||||
"label": "BOM Level",
|
||||
"fieldtype": "Data",
|
||||
"fieldname": "bom_level",
|
||||
"width": 100
|
||||
},
|
||||
{
|
||||
"label": "Standard Description",
|
||||
"fieldtype": "data",
|
||||
|
@ -70,12 +70,12 @@ def get_bom_stock(filters):
|
||||
ON bom_item.item_code = ledger.item_code
|
||||
{conditions}
|
||||
WHERE
|
||||
bom_item.parent = '{bom}' and bom_item.parenttype='BOM'
|
||||
bom_item.parent = {bom} and bom_item.parenttype='BOM'
|
||||
|
||||
GROUP BY bom_item.item_code""".format(
|
||||
qty_field=qty_field,
|
||||
table=table,
|
||||
conditions=conditions,
|
||||
bom=bom,
|
||||
bom=frappe.db.escape(bom),
|
||||
qty_to_produce=qty_to_produce or 1)
|
||||
)
|
||||
|
@ -68,6 +68,18 @@ frappe.query_reports["Job Card Summary"] = {
|
||||
get_data: function(txt) {
|
||||
return frappe.db.get_link_options('Item', txt);
|
||||
}
|
||||
},
|
||||
{
|
||||
label: __("Workstation"),
|
||||
fieldname: "workstation",
|
||||
fieldtype: "Link",
|
||||
options: "Workstation"
|
||||
},
|
||||
{
|
||||
label: __("Operation"),
|
||||
fieldname: "operation",
|
||||
fieldtype: "Link",
|
||||
options: "Operation"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
@ -1,14 +1,16 @@
|
||||
{
|
||||
"add_total_row": 0,
|
||||
"add_total_row": 1,
|
||||
"columns": [],
|
||||
"creation": "2020-04-20 12:00:21.436619",
|
||||
"disable_prepared_report": 0,
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 0,
|
||||
"is_standard": "Yes",
|
||||
"letter_head": "Gadgets International",
|
||||
"modified": "2020-04-20 12:00:21.436619",
|
||||
"letter_head": "",
|
||||
"modified": "2020-12-30 11:49:21.713561",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Job Card Summary",
|
||||
|
@ -0,0 +1,32 @@
|
||||
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
/* eslint-disable */
|
||||
|
||||
frappe.query_reports["Production Plan Summary"] = {
|
||||
"filters": [
|
||||
{
|
||||
fieldname: "production_plan",
|
||||
label: __("Production Plan"),
|
||||
fieldtype: "Link",
|
||||
options: "Production Plan",
|
||||
reqd: 1,
|
||||
get_query: function() {
|
||||
return {
|
||||
filters: {
|
||||
"docstatus": 1
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
],
|
||||
"formatter": function(value, row, column, data, default_formatter) {
|
||||
value = default_formatter(value, row, column, data);
|
||||
|
||||
if (column.fieldname == "document_name") {
|
||||
var color = data.pending_qty > 0 ? 'red': 'green';
|
||||
value = `<a style='color:${color}' href="#Form/${data['document_type']}/${data['document_name']}" data-doctype="${data['document_type']}">${data['document_name']}</a>`;
|
||||
}
|
||||
|
||||
return value;
|
||||
},
|
||||
};
|
@ -0,0 +1,26 @@
|
||||
{
|
||||
"add_total_row": 0,
|
||||
"columns": [],
|
||||
"creation": "2020-12-27 11:43:39.781793",
|
||||
"disable_prepared_report": 0,
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 0,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2020-12-27 11:43:42.677584",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Production Plan Summary",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "Production Plan",
|
||||
"report_name": "Production Plan Summary",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Manufacturing User"
|
||||
}
|
||||
]
|
||||
}
|
@ -0,0 +1,136 @@
|
||||
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
from frappe.utils import flt
|
||||
|
||||
def execute(filters=None):
|
||||
columns, data = [], []
|
||||
data = get_data(filters)
|
||||
columns = get_column(filters)
|
||||
|
||||
return columns, data
|
||||
|
||||
def get_data(filters):
|
||||
data = []
|
||||
|
||||
order_details = {}
|
||||
get_work_order_details(filters, order_details)
|
||||
get_purchase_order_details(filters, order_details)
|
||||
get_production_plan_item_details(filters, data, order_details)
|
||||
|
||||
return data
|
||||
|
||||
def get_production_plan_item_details(filters, data, order_details):
|
||||
itemwise_indent = {}
|
||||
|
||||
production_plan_doc = frappe.get_cached_doc("Production Plan", filters.get("production_plan"))
|
||||
for row in production_plan_doc.po_items:
|
||||
work_order = frappe.get_cached_value("Work Order", {"production_plan_item": row.name,
|
||||
"bom_no": row.bom_no, "production_item": row.item_code}, "name")
|
||||
|
||||
if row.item_code not in itemwise_indent:
|
||||
itemwise_indent.setdefault(row.item_code, {})
|
||||
|
||||
data.append({
|
||||
"indent": 0,
|
||||
"item_code": row.item_code,
|
||||
"item_name": frappe.get_cached_value("Item", row.item_code, "item_name"),
|
||||
"qty": row.planned_qty,
|
||||
"document_type": "Work Order",
|
||||
"document_name": work_order,
|
||||
"bom_level": frappe.get_cached_value("BOM", row.bom_no, "bom_level"),
|
||||
"produced_qty": order_details.get((work_order, row.item_code)).get("produced_qty"),
|
||||
"pending_qty": flt(row.planned_qty) - flt(order_details.get((work_order, row.item_code)).get("produced_qty"))
|
||||
})
|
||||
|
||||
get_production_plan_sub_assembly_item_details(filters, row, production_plan_doc, data, order_details)
|
||||
|
||||
def get_production_plan_sub_assembly_item_details(filters, row, production_plan_doc, data, order_details):
|
||||
for item in production_plan_doc.sub_assembly_items:
|
||||
if row.name == item.production_plan_item:
|
||||
subcontracted_item = (item.type_of_manufacturing == 'Subcontract')
|
||||
|
||||
if subcontracted_item:
|
||||
docname = frappe.get_cached_value("Purchase Order Item",
|
||||
{"production_plan_sub_assembly_item": item.name, "docstatus": ("<", 2)}, "parent")
|
||||
else:
|
||||
docname = frappe.get_cached_value("Work Order",
|
||||
{"production_plan_sub_assembly_item": item.name, "docstatus": ("<", 2)}, "name")
|
||||
|
||||
data.append({
|
||||
"indent": 1,
|
||||
"item_code": item.production_item,
|
||||
"item_name": item.item_name,
|
||||
"qty": item.qty,
|
||||
"document_type": "Work Order" if not subcontracted_item else "Purchase Order",
|
||||
"document_name": docname,
|
||||
"bom_level": item.bom_level,
|
||||
"produced_qty": order_details.get((docname, item.production_item)).get("produced_qty"),
|
||||
"pending_qty": flt(item.qty) - flt(order_details.get((docname, item.production_item)).get("produced_qty"))
|
||||
})
|
||||
|
||||
def get_work_order_details(filters, order_details):
|
||||
for row in frappe.get_all("Work Order", filters = {"production_plan": filters.get("production_plan")},
|
||||
fields=["name", "produced_qty", "production_plan", "production_item"]):
|
||||
order_details.setdefault((row.name, row.production_item), row)
|
||||
|
||||
def get_purchase_order_details(filters, order_details):
|
||||
for row in frappe.get_all("Purchase Order Item", filters = {"production_plan": filters.get("production_plan")},
|
||||
fields=["parent", "received_qty as produced_qty", "item_code"]):
|
||||
order_details.setdefault((row.parent, row.item_code), row)
|
||||
|
||||
def get_column(filters):
|
||||
return [
|
||||
{
|
||||
"label": "Finished Good",
|
||||
"fieldtype": "Link",
|
||||
"fieldname": "item_code",
|
||||
"width": 300,
|
||||
"options": "Item"
|
||||
},
|
||||
{
|
||||
"label": "Item Name",
|
||||
"fieldtype": "data",
|
||||
"fieldname": "item_name",
|
||||
"width": 100
|
||||
},
|
||||
{
|
||||
"label": "Document Type",
|
||||
"fieldtype": "Link",
|
||||
"fieldname": "document_type",
|
||||
"width": 150,
|
||||
"options": "DocType"
|
||||
},
|
||||
{
|
||||
"label": "Document Name",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"fieldname": "document_name",
|
||||
"width": 150
|
||||
},
|
||||
{
|
||||
"label": "BOM Level",
|
||||
"fieldtype": "Int",
|
||||
"fieldname": "bom_level",
|
||||
"width": 100
|
||||
},
|
||||
{
|
||||
"label": "Order Qty",
|
||||
"fieldtype": "Float",
|
||||
"fieldname": "qty",
|
||||
"width": 120
|
||||
},
|
||||
{
|
||||
"label": "Received Qty",
|
||||
"fieldtype": "Float",
|
||||
"fieldname": "produced_qty",
|
||||
"width": 160
|
||||
},
|
||||
{
|
||||
"label": "Pending Qty",
|
||||
"fieldtype": "Float",
|
||||
"fieldname": "pending_qty",
|
||||
"width": 110
|
||||
}
|
||||
]
|
@ -19,7 +19,7 @@ def execute(filters=None):
|
||||
return columns, data, None, chart_data
|
||||
|
||||
def get_data(filters):
|
||||
query_filters = {"docstatus": 1}
|
||||
query_filters = {"docstatus": ("<", 2)}
|
||||
|
||||
fields = ["name", "status", "sales_order", "production_item", "qty", "produced_qty",
|
||||
"planned_start_date", "planned_end_date", "actual_start_date", "actual_end_date", "lead_time"]
|
||||
@ -62,7 +62,8 @@ def get_chart_based_on_status(data):
|
||||
"Not Started": 0,
|
||||
"In Process": 0,
|
||||
"Stopped": 0,
|
||||
"Completed": 0
|
||||
"Completed": 0,
|
||||
"Draft": 0
|
||||
}
|
||||
|
||||
for d in data:
|
||||
|
@ -293,3 +293,5 @@ erpnext.patches.v13_0.rename_issue_status_hold_to_on_hold
|
||||
erpnext.patches.v13_0.update_response_by_variance
|
||||
erpnext.patches.v13_0.bill_for_rejected_quantity_in_purchase_invoice
|
||||
erpnext.patches.v13_0.update_job_card_details
|
||||
erpnext.patches.v13_0.update_level_in_bom #1234sswef
|
||||
erpnext.patches.v13_0.add_missing_fg_item_for_stock_entry
|
||||
|
110
erpnext/patches/v13_0/add_missing_fg_item_for_stock_entry.py
Normal file
110
erpnext/patches/v13_0/add_missing_fg_item_for_stock_entry.py
Normal file
@ -0,0 +1,110 @@
|
||||
# Copyright (c) 2020, Frappe and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.utils import cstr, flt, cint
|
||||
from erpnext.stock.stock_ledger import make_sl_entries
|
||||
from erpnext.controllers.stock_controller import create_repost_item_valuation_entry
|
||||
|
||||
def execute():
|
||||
if not frappe.db.has_column('Work Order', 'has_batch_no'):
|
||||
return
|
||||
|
||||
if cint(frappe.db.get_single_value('Manufacturing Settings', 'make_serial_no_batch_from_work_order')):
|
||||
return
|
||||
|
||||
frappe.reload_doc('manufacturing', 'doctype', 'work_order')
|
||||
filters = {
|
||||
'docstatus': 1,
|
||||
'produced_qty': ('>', 0),
|
||||
'creation': ('>=', '2021-06-29 00:00:00'),
|
||||
'has_batch_no': 1
|
||||
}
|
||||
|
||||
fields = ['name', 'production_item']
|
||||
|
||||
work_orders = [d.name for d in frappe.get_all('Work Order', filters = filters, fields=fields)]
|
||||
|
||||
if not work_orders:
|
||||
return
|
||||
|
||||
repost_stock_entries = []
|
||||
stock_entries = frappe.db.sql_list('''
|
||||
SELECT
|
||||
se.name
|
||||
FROM
|
||||
`tabStock Entry` se
|
||||
WHERE
|
||||
se.purpose = 'Manufacture' and se.docstatus < 2 and se.work_order in {work_orders}
|
||||
and not exists(
|
||||
select name from `tabStock Entry Detail` sed where sed.parent = se.name and sed.is_finished_item = 1
|
||||
)
|
||||
Order BY
|
||||
se.posting_date, se.posting_time
|
||||
'''.format(work_orders=tuple(work_orders)))
|
||||
|
||||
if stock_entries:
|
||||
print('Length of stock entries', len(stock_entries))
|
||||
|
||||
for stock_entry in stock_entries:
|
||||
doc = frappe.get_doc('Stock Entry', stock_entry)
|
||||
doc.set_work_order_details()
|
||||
doc.load_items_from_bom()
|
||||
doc.calculate_rate_and_amount()
|
||||
set_expense_account(doc)
|
||||
doc.make_batches('t_warehouse')
|
||||
|
||||
if doc.docstatus == 0:
|
||||
doc.save()
|
||||
else:
|
||||
repost_stock_entry(doc)
|
||||
repost_stock_entries.append(doc)
|
||||
|
||||
for repost_doc in repost_stock_entries:
|
||||
repost_future_sle_and_gle(repost_doc)
|
||||
|
||||
def set_expense_account(doc):
|
||||
for row in doc.items:
|
||||
if row.is_finished_item and not row.expense_account:
|
||||
row.expense_account = frappe.get_cached_value('Company', doc.company, 'stock_adjustment_account')
|
||||
|
||||
def repost_stock_entry(doc):
|
||||
doc.db_update()
|
||||
for child_row in doc.items:
|
||||
if child_row.is_finished_item:
|
||||
child_row.db_update()
|
||||
|
||||
sl_entries = []
|
||||
finished_item_row = doc.get_finished_item_row()
|
||||
get_sle_for_target_warehouse(doc, sl_entries, finished_item_row)
|
||||
|
||||
if sl_entries:
|
||||
try:
|
||||
make_sl_entries(sl_entries, True)
|
||||
except Exception:
|
||||
print(f'SLE entries not posted for the stock entry {doc.name}')
|
||||
traceback = frappe.get_traceback()
|
||||
frappe.log_error(traceback)
|
||||
|
||||
def get_sle_for_target_warehouse(doc, sl_entries, finished_item_row):
|
||||
for d in doc.get('items'):
|
||||
if cstr(d.t_warehouse) and finished_item_row and d.name == finished_item_row.name:
|
||||
sle = doc.get_sl_entries(d, {
|
||||
"warehouse": cstr(d.t_warehouse),
|
||||
"actual_qty": flt(d.transfer_qty),
|
||||
"incoming_rate": flt(d.valuation_rate)
|
||||
})
|
||||
|
||||
sle.recalculate_rate = 1
|
||||
sl_entries.append(sle)
|
||||
|
||||
def repost_future_sle_and_gle(doc):
|
||||
args = frappe._dict({
|
||||
"posting_date": doc.posting_date,
|
||||
"posting_time": doc.posting_time,
|
||||
"voucher_type": doc.doctype,
|
||||
"voucher_no": doc.name,
|
||||
"company": doc.company
|
||||
})
|
||||
|
||||
create_repost_item_valuation_entry(args)
|
30
erpnext/patches/v13_0/update_level_in_bom.py
Normal file
30
erpnext/patches/v13_0/update_level_in_bom.py
Normal file
@ -0,0 +1,30 @@
|
||||
# Copyright (c) 2020, Frappe and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
|
||||
def execute():
|
||||
for document in ["bom", "bom_item", "bom_explosion_item"]:
|
||||
frappe.reload_doc('manufacturing', 'doctype', document)
|
||||
|
||||
frappe.db.sql(" update `tabBOM` set bom_level = 0 where docstatus = 1")
|
||||
|
||||
bom_list = frappe.db.sql_list("""select name from `tabBOM` bom
|
||||
where docstatus=1 and is_active=1 and not exists(select bom_no from `tabBOM Item`
|
||||
where parent=bom.name and ifnull(bom_no, '')!='')""")
|
||||
|
||||
count = 0
|
||||
while(count < len(bom_list)):
|
||||
for parent_bom in get_parent_boms(bom_list[count]):
|
||||
bom_doc = frappe.get_cached_doc("BOM", parent_bom)
|
||||
bom_doc.set_bom_level(update=True)
|
||||
bom_list.append(parent_bom)
|
||||
count += 1
|
||||
|
||||
def get_parent_boms(bom_no):
|
||||
return frappe.db.sql_list("""
|
||||
select distinct bom_item.parent from `tabBOM Item` bom_item
|
||||
where bom_item.bom_no = %s and bom_item.docstatus=1 and bom_item.parenttype='BOM'
|
||||
and exists(select bom.name from `tabBOM` bom where bom.name=bom_item.parent and bom.is_active=1)
|
||||
""", bom_no)
|
@ -2,6 +2,7 @@ import frappe
|
||||
from frappe.utils import cint
|
||||
from erpnext.portal.product_configurator.item_variants_cache import ItemVariantsCacheManager
|
||||
from erpnext.shopping_cart.product_info import get_product_info_for_website
|
||||
from erpnext.setup.doctype.item_group.item_group import get_child_groups
|
||||
|
||||
def get_field_filter_data():
|
||||
product_settings = get_product_settings()
|
||||
@ -89,6 +90,7 @@ def get_products_for_website(field_filters=None, attribute_filters=None, search=
|
||||
def get_products_html_for_website(field_filters=None, attribute_filters=None):
|
||||
field_filters = frappe.parse_json(field_filters)
|
||||
attribute_filters = frappe.parse_json(attribute_filters)
|
||||
set_item_group_filters(field_filters)
|
||||
|
||||
items = get_products_for_website(field_filters, attribute_filters)
|
||||
html = ''.join(get_html_for_items(items))
|
||||
@ -98,6 +100,10 @@ def get_products_html_for_website(field_filters=None, attribute_filters=None):
|
||||
|
||||
return html
|
||||
|
||||
def set_item_group_filters(field_filters):
|
||||
if 'item_group' in field_filters:
|
||||
field_filters['item_group'] = [ig[0] for ig in get_child_groups(field_filters['item_group'])]
|
||||
|
||||
|
||||
def get_item_codes_by_attributes(attribute_filters, template_item_code=None):
|
||||
items = []
|
||||
|
@ -77,9 +77,6 @@ class Task(NestedSet):
|
||||
if flt(self.progress or 0) > 100:
|
||||
frappe.throw(_("Progress % for a task cannot be more than 100."))
|
||||
|
||||
if flt(self.progress) == 100:
|
||||
self.status = 'Completed'
|
||||
|
||||
if self.status == 'Completed':
|
||||
self.progress = 100
|
||||
|
||||
|
@ -67,6 +67,8 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
|
||||
calculate_discount_amount(){
|
||||
if (frappe.meta.get_docfield(this.frm.doc.doctype, "discount_amount")) {
|
||||
this.calculate_item_values();
|
||||
this.calculate_net_total();
|
||||
this.set_discount_amount();
|
||||
this.apply_discount_amount();
|
||||
}
|
||||
|
@ -54,7 +54,7 @@ frappe.help.help_links["permission-manager"] = [
|
||||
|
||||
frappe.help.help_links["Form/System Settings"] = [
|
||||
{
|
||||
label: "Naming Series",
|
||||
label: "System Settings",
|
||||
url: docsUrl + "user/manual/en/setting-up/settings/system-settings",
|
||||
},
|
||||
];
|
||||
@ -206,7 +206,7 @@ frappe.help.help_links["Form/PayPal Settings"] = [
|
||||
label: "PayPal Settings",
|
||||
url:
|
||||
docsUrl +
|
||||
"user/manual/en/setting-up/integrations/paypal-integration",
|
||||
"user/manual/en/erpnext_integration/paypal-integration",
|
||||
},
|
||||
];
|
||||
|
||||
@ -215,14 +215,14 @@ frappe.help.help_links["Form/Razorpay Settings"] = [
|
||||
label: "Razorpay Settings",
|
||||
url:
|
||||
docsUrl +
|
||||
"user/manual/en/setting-up/integrations/razorpay-integration",
|
||||
"user/manual/en/erpnext_integration/razorpay-integration",
|
||||
},
|
||||
];
|
||||
|
||||
frappe.help.help_links["Form/Dropbox Settings"] = [
|
||||
{
|
||||
label: "Dropbox Settings",
|
||||
url: docsUrl + "user/manual/en/setting-up/integrations/dropbox-backup",
|
||||
url: docsUrl + "user/manual/en/erpnext_integration/dropbox-backup",
|
||||
},
|
||||
];
|
||||
|
||||
@ -230,7 +230,7 @@ frappe.help.help_links["Form/LDAP Settings"] = [
|
||||
{
|
||||
label: "LDAP Settings",
|
||||
url:
|
||||
docsUrl + "user/manual/en/setting-up/integrations/ldap-integration",
|
||||
docsUrl + "user/manual/en/erpnext_integration/ldap-integration",
|
||||
},
|
||||
];
|
||||
|
||||
@ -239,7 +239,7 @@ frappe.help.help_links["Form/Stripe Settings"] = [
|
||||
label: "Stripe Settings",
|
||||
url:
|
||||
docsUrl +
|
||||
"user/manual/en/setting-up/integrations/stripe-integration",
|
||||
"user/manual/en/erpnext_integration/stripe-integration",
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -147,7 +147,7 @@ erpnext.setup.slides_settings = [
|
||||
}
|
||||
|
||||
// Validate bank name
|
||||
if(me.values.bank_account){
|
||||
if(me.values.bank_account) {
|
||||
frappe.call({
|
||||
async: false,
|
||||
method: "erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts.validate_bank_account",
|
||||
|
@ -19,6 +19,21 @@ class GSTSettings(Document):
|
||||
from tabAddress where country = "India" and ifnull(gstin, '')!='' ''')
|
||||
self.set_onload('data', data)
|
||||
|
||||
def validate(self):
|
||||
# Validate duplicate accounts
|
||||
self.validate_duplicate_accounts()
|
||||
|
||||
def validate_duplicate_accounts(self):
|
||||
account_list = []
|
||||
for account in self.get('gst_accounts'):
|
||||
for fieldname in ['cgst_account', 'sgst_account', 'igst_account', 'cess_account']:
|
||||
if account.get(fieldname) in account_list:
|
||||
frappe.throw(_("Account {0} appears multiple times").format(
|
||||
frappe.bold(account.get(fieldname))))
|
||||
|
||||
if account.get(fieldname):
|
||||
account_list.append(account.get(fieldname))
|
||||
|
||||
@frappe.whitelist()
|
||||
def send_reminder():
|
||||
frappe.has_permission('GST Settings', throw=True)
|
||||
|
@ -46,14 +46,14 @@ class TestGSTR3BReport(unittest.TestCase):
|
||||
make_sales_invoice()
|
||||
create_purchase_invoices()
|
||||
|
||||
if frappe.db.exists("GSTR 3B Report", "GSTR3B-March-2019-_Test Address-Billing"):
|
||||
report = frappe.get_doc("GSTR 3B Report", "GSTR3B-March-2019-_Test Address-Billing")
|
||||
if frappe.db.exists("GSTR 3B Report", "GSTR3B-March-2019-_Test Address GST-Billing"):
|
||||
report = frappe.get_doc("GSTR 3B Report", "GSTR3B-March-2019-_Test Address GST-Billing")
|
||||
report.save()
|
||||
else:
|
||||
report = frappe.get_doc({
|
||||
"doctype": "GSTR 3B Report",
|
||||
"company": "_Test Company GST",
|
||||
"company_address": "_Test Address-Billing",
|
||||
"company_address": "_Test Address GST-Billing",
|
||||
"year": getdate().year,
|
||||
"month": month_number_mapping.get(getdate().month)
|
||||
}).insert()
|
||||
@ -89,7 +89,7 @@ class TestGSTR3BReport(unittest.TestCase):
|
||||
|
||||
si.append("taxes", {
|
||||
"charge_type": "On Net Total",
|
||||
"account_head": "IGST - _GST",
|
||||
"account_head": "Output Tax IGST - _GST",
|
||||
"cost_center": "Main - _GST",
|
||||
"description": "IGST @ 18.0",
|
||||
"rate": 18
|
||||
@ -117,7 +117,7 @@ def make_sales_invoice():
|
||||
|
||||
si.append("taxes", {
|
||||
"charge_type": "On Net Total",
|
||||
"account_head": "IGST - _GST",
|
||||
"account_head": "Output Tax IGST - _GST",
|
||||
"cost_center": "Main - _GST",
|
||||
"description": "IGST @ 18.0",
|
||||
"rate": 18
|
||||
@ -138,7 +138,7 @@ def make_sales_invoice():
|
||||
|
||||
si1.append("taxes", {
|
||||
"charge_type": "On Net Total",
|
||||
"account_head": "IGST - _GST",
|
||||
"account_head": "Output Tax IGST - _GST",
|
||||
"cost_center": "Main - _GST",
|
||||
"description": "IGST @ 18.0",
|
||||
"rate": 18
|
||||
@ -159,7 +159,7 @@ def make_sales_invoice():
|
||||
|
||||
si2.append("taxes", {
|
||||
"charge_type": "On Net Total",
|
||||
"account_head": "IGST - _GST",
|
||||
"account_head": "Output Tax IGST - _GST",
|
||||
"cost_center": "Main - _GST",
|
||||
"description": "IGST @ 18.0",
|
||||
"rate": 18
|
||||
@ -195,7 +195,7 @@ def create_purchase_invoices():
|
||||
|
||||
pi.append("taxes", {
|
||||
"charge_type": "On Net Total",
|
||||
"account_head": "CGST - _GST",
|
||||
"account_head": "Input Tax CGST - _GST",
|
||||
"cost_center": "Main - _GST",
|
||||
"description": "CGST @ 9.0",
|
||||
"rate": 9
|
||||
@ -203,7 +203,7 @@ def create_purchase_invoices():
|
||||
|
||||
pi.append("taxes", {
|
||||
"charge_type": "On Net Total",
|
||||
"account_head": "SGST - _GST",
|
||||
"account_head": "Input Tax SGST - _GST",
|
||||
"cost_center": "Main - _GST",
|
||||
"description": "SGST @ 9.0",
|
||||
"rate": 9
|
||||
@ -410,10 +410,10 @@ def make_company():
|
||||
company.country = "India"
|
||||
company.insert()
|
||||
|
||||
if not frappe.db.exists('Address', '_Test Address-Billing'):
|
||||
if not frappe.db.exists('Address', '_Test Address GST-Billing'):
|
||||
address = frappe.get_doc({
|
||||
"address_title": "_Test Address GST",
|
||||
"address_line1": "_Test Address Line 1",
|
||||
"address_title": "_Test Address",
|
||||
"address_type": "Billing",
|
||||
"city": "_Test City",
|
||||
"state": "Test State",
|
||||
@ -444,9 +444,9 @@ def set_account_heads():
|
||||
if not gst_account:
|
||||
gst_settings.append("gst_accounts", {
|
||||
"company": "_Test Company GST",
|
||||
"cgst_account": "CGST - _GST",
|
||||
"sgst_account": "SGST - _GST",
|
||||
"igst_account": "IGST - _GST",
|
||||
"cgst_account": "Output Tax CGST - _GST",
|
||||
"sgst_account": "Output Tax SGST - _GST",
|
||||
"igst_account": "Output Tax IGST - _GST"
|
||||
})
|
||||
|
||||
gst_settings.save()
|
||||
|
@ -12,7 +12,10 @@ from erpnext.accounts.utils import get_fiscal_year, FiscalYearError
|
||||
from frappe.utils import today
|
||||
|
||||
def setup(company=None, patch=True):
|
||||
setup_company_independent_fixtures(patch=patch)
|
||||
# Company independent fixtures should be called only once at the first company setup
|
||||
if frappe.db.count('Company', {'country': 'India'}) <=1:
|
||||
setup_company_independent_fixtures(patch=patch)
|
||||
|
||||
if not patch:
|
||||
make_fixtures(company)
|
||||
|
||||
@ -25,6 +28,7 @@ def setup_company_independent_fixtures(patch=False):
|
||||
frappe.enqueue('erpnext.regional.india.setup.add_hsn_sac_codes', now=frappe.flags.in_test)
|
||||
create_gratuity_rule()
|
||||
add_print_formats()
|
||||
update_accounts_settings_for_taxes()
|
||||
|
||||
def add_hsn_sac_codes():
|
||||
if frappe.flags.in_test and frappe.flags.created_hsn_codes:
|
||||
@ -121,10 +125,12 @@ def add_print_formats():
|
||||
def make_property_setters(patch=False):
|
||||
# GST rules do not allow for an invoice no. bigger than 16 characters
|
||||
journal_entry_types = frappe.get_meta("Journal Entry").get_options("voucher_type").split("\n") + ['Reversal Of ITC']
|
||||
sales_invoice_series = ['SINV-.YY.-', 'SRET-.YY.-', ''] + frappe.get_meta("Sales Invoice").get_options("naming_series").split("\n")
|
||||
purchase_invoice_series = ['PINV-.YY.-', 'PRET-.YY.-', ''] + frappe.get_meta("Purchase Invoice").get_options("naming_series").split("\n")
|
||||
|
||||
if not patch:
|
||||
make_property_setter('Sales Invoice', 'naming_series', 'options', 'SINV-.YY.-\nSRET-.YY.-', '')
|
||||
make_property_setter('Purchase Invoice', 'naming_series', 'options', 'PINV-.YY.-\nPRET-.YY.-', '')
|
||||
make_property_setter('Sales Invoice', 'naming_series', 'options', '\n'.join(sales_invoice_series), '')
|
||||
make_property_setter('Purchase Invoice', 'naming_series', 'options', '\n'.join(purchase_invoice_series), '')
|
||||
make_property_setter('Journal Entry', 'voucher_type', 'options', '\n'.join(journal_entry_types), '')
|
||||
|
||||
def make_custom_fields(update=True):
|
||||
@ -680,7 +686,7 @@ def make_custom_fields(update=True):
|
||||
|
||||
def make_fixtures(company=None):
|
||||
docs = []
|
||||
company = company.name if company else frappe.db.get_value("Global Defaults", None, "default_company")
|
||||
company = company or frappe.db.get_value("Global Defaults", None, "default_company")
|
||||
|
||||
set_salary_components(docs)
|
||||
set_tds_account(docs, company)
|
||||
@ -698,6 +704,53 @@ def make_fixtures(company=None):
|
||||
# create records for Tax Withholding Category
|
||||
set_tax_withholding_category(company)
|
||||
|
||||
def update_regional_tax_settings(country, company):
|
||||
# Will only add default GST accounts if present
|
||||
input_account_names = ['Input Tax CGST', 'Input Tax SGST', 'Input Tax IGST']
|
||||
output_account_names = ['Output Tax CGST', 'Output Tax SGST', 'Output Tax IGST']
|
||||
rcm_accounts = ['Input Tax CGST RCM', 'Input Tax SGST RCM', 'Input Tax IGST RCM']
|
||||
gst_settings = frappe.get_single('GST Settings')
|
||||
existing_account_list = []
|
||||
|
||||
for account in gst_settings.get('gst_accounts'):
|
||||
for key in ['cgst_account', 'sgst_account', 'igst_account']:
|
||||
existing_account_list.append(account.get(key))
|
||||
|
||||
gst_accounts = frappe._dict(frappe.get_all("Account",
|
||||
{'company': company, 'account_name': ('in', input_account_names +
|
||||
output_account_names + rcm_accounts)}, ['account_name', 'name'], as_list=1))
|
||||
|
||||
add_accounts_in_gst_settings(company, input_account_names, gst_accounts,
|
||||
existing_account_list, gst_settings)
|
||||
add_accounts_in_gst_settings(company, output_account_names, gst_accounts,
|
||||
existing_account_list, gst_settings)
|
||||
add_accounts_in_gst_settings(company, rcm_accounts, gst_accounts,
|
||||
existing_account_list, gst_settings, is_reverse_charge=1)
|
||||
|
||||
gst_settings.save()
|
||||
|
||||
def add_accounts_in_gst_settings(company, account_names, gst_accounts,
|
||||
existing_account_list, gst_settings, is_reverse_charge=0):
|
||||
accounts_not_added = 1
|
||||
|
||||
for account in account_names:
|
||||
# Default Account Added does not exists
|
||||
if not gst_accounts.get(account):
|
||||
accounts_not_added = 0
|
||||
|
||||
# Check if already added in GST Settings
|
||||
if gst_accounts.get(account) in existing_account_list:
|
||||
accounts_not_added = 0
|
||||
|
||||
if accounts_not_added:
|
||||
gst_settings.append('gst_accounts', {
|
||||
'company': company,
|
||||
'cgst_account': gst_accounts.get(account_names[0]),
|
||||
'sgst_account': gst_accounts.get(account_names[1]),
|
||||
'igst_account': gst_accounts.get(account_names[2]),
|
||||
'is_reverse_charge_account': is_reverse_charge
|
||||
})
|
||||
|
||||
def set_salary_components(docs):
|
||||
docs.extend([
|
||||
{'doctype': 'Salary Component', 'salary_component': 'Professional Tax',
|
||||
@ -731,13 +784,14 @@ def set_tax_withholding_category(company):
|
||||
docs = get_tds_details(accounts, fiscal_year)
|
||||
|
||||
for d in docs:
|
||||
try:
|
||||
if not frappe.db.exists("Tax Withholding Category", d.get("name")):
|
||||
doc = frappe.get_doc(d)
|
||||
doc.flags.ignore_validate = True
|
||||
doc.flags.ignore_permissions = True
|
||||
doc.flags.ignore_mandatory = True
|
||||
doc.insert()
|
||||
except frappe.DuplicateEntryError:
|
||||
doc = frappe.get_doc("Tax Withholding Category", d.get("name"))
|
||||
else:
|
||||
doc = frappe.get_doc("Tax Withholding Category", d.get("name"), for_update=True)
|
||||
|
||||
if accounts:
|
||||
doc.append("accounts", accounts[0])
|
||||
@ -749,11 +803,12 @@ def set_tax_withholding_category(company):
|
||||
doc.append("rates", d.get('rates')[0])
|
||||
|
||||
doc.flags.ignore_permissions = True
|
||||
doc.flags.ignore_validate = True
|
||||
doc.flags.ignore_mandatory = True
|
||||
doc.flags.ignore_links = True
|
||||
doc.save()
|
||||
|
||||
def set_tds_account(docs, company):
|
||||
abbr = frappe.get_value("Company", company, "abbr")
|
||||
parent_account = frappe.db.get_value("Account", filters = {"account_name": "Duties and Taxes", "company": company})
|
||||
if parent_account:
|
||||
docs.extend([
|
||||
@ -912,7 +967,6 @@ def get_tds_details(accounts, fiscal_year):
|
||||
]
|
||||
|
||||
def create_gratuity_rule():
|
||||
|
||||
# Standard Indain Gratuity Rule
|
||||
if not frappe.db.exists("Gratuity Rule", "Indian Standard Gratuity Rule"):
|
||||
rule = frappe.new_doc("Gratuity Rule")
|
||||
@ -930,3 +984,7 @@ def create_gratuity_rule():
|
||||
|
||||
rule.flags.ignore_mandatory = True
|
||||
rule.save()
|
||||
|
||||
def update_accounts_settings_for_taxes():
|
||||
if frappe.db.count('Company') == 1:
|
||||
frappe.db.set_value('Accounts Settings', None, "add_taxes_from_item_tax_template", 0)
|
@ -11,7 +11,7 @@
|
||||
"is_standard": "Yes",
|
||||
"json": "{}",
|
||||
"letter_head": "Logo",
|
||||
"modified": "2021-03-12 12:36:48.689413",
|
||||
"modified": "2021-03-13 12:36:48.689413",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Regional",
|
||||
"name": "E-Invoice Summary",
|
||||
|
@ -584,7 +584,7 @@ class Gstr1Report(object):
|
||||
def get_json(filters, report_name, data):
|
||||
filters = json.loads(filters)
|
||||
report_data = json.loads(data)
|
||||
gstin = get_company_gstin_number(filters["company"], filters["company_address"])
|
||||
gstin = get_company_gstin_number(filters.get("company"), filters.get("company_address"))
|
||||
|
||||
fp = "%02d%s" % (getdate(filters["to_date"]).month, getdate(filters["to_date"]).year)
|
||||
|
||||
|
@ -367,15 +367,16 @@ erpnext.PointOfSale.ItemCart = class {
|
||||
`<div class="add-discount-field"></div>`
|
||||
);
|
||||
const me = this;
|
||||
const frm = me.events.get_frm();
|
||||
let discount = frm.doc.additional_discount_percentage;
|
||||
|
||||
this.discount_field = frappe.ui.form.make_control({
|
||||
df: {
|
||||
label: __('Discount'),
|
||||
fieldtype: 'Data',
|
||||
placeholder: __('Enter discount percentage.'),
|
||||
placeholder: ( discount ? discount + '%' : __('Enter discount percentage.') ),
|
||||
input_class: 'input-xs',
|
||||
onchange: function() {
|
||||
const frm = me.events.get_frm();
|
||||
if (flt(this.value) != 0) {
|
||||
frappe.model.set_value(frm.doc.doctype, frm.doc.name, 'additional_discount_percentage', flt(this.value));
|
||||
me.hide_discount_control(this.value);
|
||||
|
@ -110,7 +110,7 @@ class Company(NestedSet):
|
||||
self.create_default_warehouses()
|
||||
|
||||
if frappe.flags.country_change:
|
||||
install_country_fixtures(self.name)
|
||||
install_country_fixtures(self.name, self.country)
|
||||
self.create_default_tax_template()
|
||||
|
||||
if not frappe.db.get_value("Department", {"company": self.name}):
|
||||
@ -291,7 +291,7 @@ class Company(NestedSet):
|
||||
cash = frappe.db.get_value('Mode of Payment', {'type': 'Cash'}, 'name')
|
||||
if cash and self.default_cash_account \
|
||||
and not frappe.db.get_value('Mode of Payment Account', {'company': self.name, 'parent': cash}):
|
||||
mode_of_payment = frappe.get_doc('Mode of Payment', cash)
|
||||
mode_of_payment = frappe.get_doc('Mode of Payment', cash, for_update=True)
|
||||
mode_of_payment.append('accounts', {
|
||||
'company': self.name,
|
||||
'default_account': self.default_cash_account
|
||||
@ -440,16 +440,15 @@ def get_name_with_abbr(name, company):
|
||||
|
||||
return " - ".join(parts)
|
||||
|
||||
def install_country_fixtures(company):
|
||||
company_doc = frappe.get_doc("Company", company)
|
||||
path = frappe.get_app_path('erpnext', 'regional', frappe.scrub(company_doc.country))
|
||||
def install_country_fixtures(company, country):
|
||||
path = frappe.get_app_path('erpnext', 'regional', frappe.scrub(country))
|
||||
if os.path.exists(path.encode("utf-8")):
|
||||
try:
|
||||
module_name = "erpnext.regional.{0}.setup.setup".format(frappe.scrub(company_doc.country))
|
||||
frappe.get_attr(module_name)(company_doc, False)
|
||||
module_name = "erpnext.regional.{0}.setup.setup".format(frappe.scrub(country))
|
||||
frappe.get_attr(module_name)(company, False)
|
||||
except Exception as e:
|
||||
frappe.log_error()
|
||||
frappe.throw(_("Failed to setup defaults for country {0}. Please contact support@erpnext.com").format(frappe.bold(company_doc.country)))
|
||||
frappe.throw(_("Failed to setup defaults for country {0}. Please contact support@erpnext.com").format(frappe.bold(country)))
|
||||
|
||||
|
||||
def update_company_current_month_sales(company):
|
||||
|
@ -87,8 +87,8 @@ class ItemGroup(NestedSet, WebsiteGenerator):
|
||||
if not field_filters:
|
||||
field_filters = {}
|
||||
|
||||
# Ensure the query remains within current item group
|
||||
field_filters['item_group'] = self.name
|
||||
# Ensure the query remains within current item group & sub group
|
||||
field_filters['item_group'] = [ig[0] for ig in get_child_groups(self.name)]
|
||||
|
||||
engine = ProductQuery()
|
||||
context.items = engine.query(attribute_filters, field_filters, search, start, item_group=self.name)
|
||||
|
@ -12,10 +12,14 @@ from frappe.desk.notifications import clear_notifications
|
||||
class TransactionDeletionRecord(Document):
|
||||
def validate(self):
|
||||
frappe.only_for('System Manager')
|
||||
self.validate_doctypes_to_be_ignored()
|
||||
|
||||
def validate_doctypes_to_be_ignored(self):
|
||||
doctypes_to_be_ignored_list = get_doctypes_to_be_ignored()
|
||||
for doctype in self.doctypes_to_be_ignored:
|
||||
if doctype.doctype_name not in doctypes_to_be_ignored_list:
|
||||
frappe.throw(_("DocTypes should not be added manually to the 'Excluded DocTypes' table. You are only allowed to remove entries from it. "), title=_("Not Allowed"))
|
||||
frappe.throw(_("DocTypes should not be added manually to the 'Excluded DocTypes' table. You are only allowed to remove entries from it. "),
|
||||
title=_("Not Allowed"))
|
||||
|
||||
def before_submit(self):
|
||||
if not self.doctypes_to_be_ignored:
|
||||
@ -23,54 +27,9 @@ class TransactionDeletionRecord(Document):
|
||||
|
||||
self.delete_bins()
|
||||
self.delete_lead_addresses()
|
||||
|
||||
company_obj = frappe.get_doc('Company', self.company)
|
||||
# reset company values
|
||||
company_obj.total_monthly_sales = 0
|
||||
company_obj.sales_monthly_history = None
|
||||
company_obj.save()
|
||||
# Clear notification counts
|
||||
self.reset_company_values()
|
||||
clear_notifications()
|
||||
|
||||
singles = frappe.get_all('DocType', filters = {'issingle': 1}, pluck = 'name')
|
||||
tables = frappe.get_all('DocType', filters = {'istable': 1}, pluck = 'name')
|
||||
doctypes_to_be_ignored_list = singles
|
||||
for doctype in self.doctypes_to_be_ignored:
|
||||
doctypes_to_be_ignored_list.append(doctype.doctype_name)
|
||||
|
||||
docfields = frappe.get_all('DocField',
|
||||
filters = {
|
||||
'fieldtype': 'Link',
|
||||
'options': 'Company',
|
||||
'parent': ['not in', doctypes_to_be_ignored_list]},
|
||||
fields=['parent', 'fieldname'])
|
||||
|
||||
for docfield in docfields:
|
||||
if docfield['parent'] != self.doctype:
|
||||
no_of_docs = frappe.db.count(docfield['parent'], {
|
||||
docfield['fieldname'] : self.company
|
||||
})
|
||||
|
||||
if no_of_docs > 0:
|
||||
self.delete_version_log(docfield['parent'], docfield['fieldname'])
|
||||
self.delete_communications(docfield['parent'], docfield['fieldname'])
|
||||
|
||||
# populate DocTypes table
|
||||
if docfield['parent'] not in tables:
|
||||
self.append('doctypes', {
|
||||
'doctype_name' : docfield['parent'],
|
||||
'no_of_docs' : no_of_docs
|
||||
})
|
||||
|
||||
# delete the docs linked with the specified company
|
||||
frappe.db.delete(docfield['parent'], {
|
||||
docfield['fieldname'] : self.company
|
||||
})
|
||||
|
||||
naming_series = frappe.db.get_value('DocType', docfield['parent'], 'autoname')
|
||||
if naming_series:
|
||||
if '#' in naming_series:
|
||||
self.update_naming_series(naming_series, docfield['parent'])
|
||||
self.delete_company_transactions()
|
||||
|
||||
def populate_doctypes_to_be_ignored_table(self):
|
||||
doctypes_to_be_ignored_list = get_doctypes_to_be_ignored()
|
||||
@ -79,6 +38,111 @@ class TransactionDeletionRecord(Document):
|
||||
'doctype_name' : doctype
|
||||
})
|
||||
|
||||
def delete_bins(self):
|
||||
frappe.db.sql("""delete from tabBin where warehouse in
|
||||
(select name from tabWarehouse where company=%s)""", self.company)
|
||||
|
||||
def delete_lead_addresses(self):
|
||||
"""Delete addresses to which leads are linked"""
|
||||
leads = frappe.get_all('Lead', filters={'company': self.company})
|
||||
leads = ["'%s'" % row.get("name") for row in leads]
|
||||
addresses = []
|
||||
if leads:
|
||||
addresses = frappe.db.sql_list("""select parent from `tabDynamic Link` where link_name
|
||||
in ({leads})""".format(leads=",".join(leads)))
|
||||
|
||||
if addresses:
|
||||
addresses = ["%s" % frappe.db.escape(addr) for addr in addresses]
|
||||
|
||||
frappe.db.sql("""delete from tabAddress where name in ({addresses}) and
|
||||
name not in (select distinct dl1.parent from `tabDynamic Link` dl1
|
||||
inner join `tabDynamic Link` dl2 on dl1.parent=dl2.parent
|
||||
and dl1.link_doctype<>dl2.link_doctype)""".format(addresses=",".join(addresses)))
|
||||
|
||||
frappe.db.sql("""delete from `tabDynamic Link` where link_doctype='Lead'
|
||||
and parenttype='Address' and link_name in ({leads})""".format(leads=",".join(leads)))
|
||||
|
||||
frappe.db.sql("""update tabCustomer set lead_name=NULL where lead_name in ({leads})""".format(leads=",".join(leads)))
|
||||
|
||||
def reset_company_values(self):
|
||||
company_obj = frappe.get_doc('Company', self.company)
|
||||
company_obj.total_monthly_sales = 0
|
||||
company_obj.sales_monthly_history = None
|
||||
company_obj.save()
|
||||
|
||||
def delete_company_transactions(self):
|
||||
doctypes_to_be_ignored_list = self.get_doctypes_to_be_ignored_list()
|
||||
docfields = self.get_doctypes_with_company_field(doctypes_to_be_ignored_list)
|
||||
|
||||
tables = self.get_all_child_doctypes()
|
||||
for docfield in docfields:
|
||||
if docfield['parent'] != self.doctype:
|
||||
no_of_docs = self.get_number_of_docs_linked_with_specified_company(docfield['parent'], docfield['fieldname'])
|
||||
|
||||
if no_of_docs > 0:
|
||||
self.delete_version_log(docfield['parent'], docfield['fieldname'])
|
||||
self.delete_communications(docfield['parent'], docfield['fieldname'])
|
||||
self.populate_doctypes_table(tables, docfield['parent'], no_of_docs)
|
||||
|
||||
self.delete_child_tables(docfield['parent'], docfield['fieldname'])
|
||||
self.delete_docs_linked_with_specified_company(docfield['parent'], docfield['fieldname'])
|
||||
|
||||
naming_series = frappe.db.get_value('DocType', docfield['parent'], 'autoname')
|
||||
if naming_series:
|
||||
if '#' in naming_series:
|
||||
self.update_naming_series(naming_series, docfield['parent'])
|
||||
|
||||
def get_doctypes_to_be_ignored_list(self):
|
||||
singles = frappe.get_all('DocType', filters = {'issingle': 1}, pluck = 'name')
|
||||
doctypes_to_be_ignored_list = singles
|
||||
for doctype in self.doctypes_to_be_ignored:
|
||||
doctypes_to_be_ignored_list.append(doctype.doctype_name)
|
||||
|
||||
return doctypes_to_be_ignored_list
|
||||
|
||||
def get_doctypes_with_company_field(self, doctypes_to_be_ignored_list):
|
||||
docfields = frappe.get_all('DocField',
|
||||
filters = {
|
||||
'fieldtype': 'Link',
|
||||
'options': 'Company',
|
||||
'parent': ['not in', doctypes_to_be_ignored_list]},
|
||||
fields=['parent', 'fieldname'])
|
||||
|
||||
return docfields
|
||||
|
||||
def get_all_child_doctypes(self):
|
||||
return frappe.get_all('DocType', filters = {'istable': 1}, pluck = 'name')
|
||||
|
||||
def get_number_of_docs_linked_with_specified_company(self, doctype, company_fieldname):
|
||||
return frappe.db.count(doctype, {company_fieldname : self.company})
|
||||
|
||||
def populate_doctypes_table(self, tables, doctype, no_of_docs):
|
||||
if doctype not in tables:
|
||||
self.append('doctypes', {
|
||||
'doctype_name' : doctype,
|
||||
'no_of_docs' : no_of_docs
|
||||
})
|
||||
|
||||
def delete_child_tables(self, doctype, company_fieldname):
|
||||
parent_docs_to_be_deleted = frappe.get_all(doctype, {
|
||||
company_fieldname : self.company
|
||||
}, pluck = 'name')
|
||||
|
||||
child_tables = frappe.get_all('DocField', filters = {
|
||||
'fieldtype': 'Table',
|
||||
'parent': doctype
|
||||
}, pluck = 'options')
|
||||
|
||||
for table in child_tables:
|
||||
frappe.db.delete(table, {
|
||||
'parent': ['in', parent_docs_to_be_deleted]
|
||||
})
|
||||
|
||||
def delete_docs_linked_with_specified_company(self, doctype, company_fieldname):
|
||||
frappe.db.delete(doctype, {
|
||||
company_fieldname : self.company
|
||||
})
|
||||
|
||||
def update_naming_series(self, naming_series, doctype_name):
|
||||
if '.' in naming_series:
|
||||
prefix, hashes = naming_series.rsplit('.', 1)
|
||||
@ -107,32 +171,6 @@ class TransactionDeletionRecord(Document):
|
||||
|
||||
frappe.delete_doc('Communication', communication_names, ignore_permissions=True)
|
||||
|
||||
def delete_bins(self):
|
||||
frappe.db.sql("""delete from tabBin where warehouse in
|
||||
(select name from tabWarehouse where company=%s)""", self.company)
|
||||
|
||||
def delete_lead_addresses(self):
|
||||
"""Delete addresses to which leads are linked"""
|
||||
leads = frappe.get_all('Lead', filters={'company': self.company})
|
||||
leads = ["'%s'" % row.get("name") for row in leads]
|
||||
addresses = []
|
||||
if leads:
|
||||
addresses = frappe.db.sql_list("""select parent from `tabDynamic Link` where link_name
|
||||
in ({leads})""".format(leads=",".join(leads)))
|
||||
|
||||
if addresses:
|
||||
addresses = ["%s" % frappe.db.escape(addr) for addr in addresses]
|
||||
|
||||
frappe.db.sql("""delete from tabAddress where name in ({addresses}) and
|
||||
name not in (select distinct dl1.parent from `tabDynamic Link` dl1
|
||||
inner join `tabDynamic Link` dl2 on dl1.parent=dl2.parent
|
||||
and dl1.link_doctype<>dl2.link_doctype)""".format(addresses=",".join(addresses)))
|
||||
|
||||
frappe.db.sql("""delete from `tabDynamic Link` where link_doctype='Lead'
|
||||
and parenttype='Address' and link_name in ({leads})""".format(leads=",".join(leads)))
|
||||
|
||||
frappe.db.sql("""update tabCustomer set lead_name=NULL where lead_name in ({leads})""".format(leads=",".join(leads)))
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_doctypes_to_be_ignored():
|
||||
doctypes_to_be_ignored_list = ['Account', 'Cost Center', 'Warehouse', 'Budget',
|
||||
|
@ -1164,33 +1164,292 @@
|
||||
},
|
||||
|
||||
"India": {
|
||||
"tax_categories": [
|
||||
{
|
||||
"title": "In-State",
|
||||
"is_inter_state": 0,
|
||||
"gst_state": ""
|
||||
},
|
||||
{
|
||||
"title": "Out-State",
|
||||
"is_inter_state": 1,
|
||||
"gst_state": ""
|
||||
},
|
||||
{
|
||||
"title": "Reverse Charge In-State",
|
||||
"is_inter_state": 0,
|
||||
"gst_state": ""
|
||||
},
|
||||
{
|
||||
"title": "Reverse Charge Out-State",
|
||||
"is_inter_state": 1,
|
||||
"gst_state": ""
|
||||
},
|
||||
{
|
||||
"title": "Registered Composition",
|
||||
"is_inter_state": 0,
|
||||
"gst_state": ""
|
||||
}
|
||||
],
|
||||
"chart_of_accounts": {
|
||||
"*": {
|
||||
"item_tax_templates": [
|
||||
{
|
||||
"title": "In State GST",
|
||||
"title": "GST 9%",
|
||||
"taxes": [
|
||||
{
|
||||
"tax_type": {
|
||||
"account_name": "SGST",
|
||||
"account_name": "Output Tax SGST",
|
||||
"tax_rate": 9.00
|
||||
}
|
||||
},
|
||||
{
|
||||
"tax_type": {
|
||||
"account_name": "CGST",
|
||||
"account_name": "Output Tax CGST",
|
||||
"tax_rate": 9.00
|
||||
}
|
||||
},
|
||||
{
|
||||
"tax_type": {
|
||||
"account_name": "Output Tax IGST",
|
||||
"tax_rate": 18.00
|
||||
}
|
||||
},
|
||||
{
|
||||
"tax_type": {
|
||||
"account_name": "Input Tax SGST",
|
||||
"tax_rate": 9.00,
|
||||
"root_type": "Asset"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tax_type": {
|
||||
"account_name": "Input Tax CGST",
|
||||
"tax_rate": 9.00,
|
||||
"root_type": "Asset"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tax_type": {
|
||||
"account_name": "Input Tax IGST",
|
||||
"tax_rate": 18.00,
|
||||
"root_type": "Asset"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tax_type": {
|
||||
"account_name": "Input Tax SGST RCM",
|
||||
"tax_rate": 9.00,
|
||||
"root_type": "Asset"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tax_type": {
|
||||
"account_name": "Input Tax CGST RCM",
|
||||
"tax_rate": 9.00,
|
||||
"root_type": "Asset"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tax_type": {
|
||||
"account_name": "Input Tax IGST RCM",
|
||||
"tax_rate": 18.00,
|
||||
"root_type": "Asset"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Out of State GST",
|
||||
"title": "GST 5%",
|
||||
"taxes": [
|
||||
{
|
||||
"tax_type": {
|
||||
"account_name": "IGST",
|
||||
"tax_rate": 18.00
|
||||
"account_name": "Output Tax SGST",
|
||||
"tax_rate": 2.5
|
||||
}
|
||||
},
|
||||
{
|
||||
"tax_type": {
|
||||
"account_name": "Output Tax CGST",
|
||||
"tax_rate": 2.5
|
||||
}
|
||||
},
|
||||
{
|
||||
"tax_type": {
|
||||
"account_name": "Output Tax IGST",
|
||||
"tax_rate": 5.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"tax_type": {
|
||||
"account_name": "Input Tax SGST",
|
||||
"tax_rate": 2.5,
|
||||
"root_type": "Asset"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tax_type": {
|
||||
"account_name": "Input Tax CGST",
|
||||
"tax_rate": 2.5,
|
||||
"root_type": "Asset"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tax_type": {
|
||||
"account_name": "Input Tax IGST",
|
||||
"tax_rate": 5.0,
|
||||
"root_type": "Asset"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tax_type": {
|
||||
"account_name": "Input Tax SGST RCM",
|
||||
"tax_rate": 2.50,
|
||||
"root_type": "Asset"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tax_type": {
|
||||
"account_name": "Input Tax CGST RCM",
|
||||
"tax_rate": 2.50,
|
||||
"root_type": "Asset"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tax_type": {
|
||||
"account_name": "Input Tax IGST RCM",
|
||||
"tax_rate": 5.00,
|
||||
"root_type": "Asset"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "GST 12%",
|
||||
"taxes": [
|
||||
{
|
||||
"tax_type": {
|
||||
"account_name": "Output Tax SGST",
|
||||
"tax_rate": 6.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"tax_type": {
|
||||
"account_name": "Output Tax CGST",
|
||||
"tax_rate": 6.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"tax_type": {
|
||||
"account_name": "Output Tax IGST",
|
||||
"tax_rate": 12.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"tax_type": {
|
||||
"account_name": "Input Tax SGST",
|
||||
"tax_rate": 6.0,
|
||||
"root_type": "Asset"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tax_type": {
|
||||
"account_name": "Input Tax CGST",
|
||||
"tax_rate": 6.0,
|
||||
"root_type": "Asset"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tax_type": {
|
||||
"account_name": "Input Tax IGST",
|
||||
"tax_rate": 12.0,
|
||||
"root_type": "Asset"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tax_type": {
|
||||
"account_name": "Input Tax SGST RCM",
|
||||
"tax_rate": 6.00,
|
||||
"root_type": "Asset"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tax_type": {
|
||||
"account_name": "Input Tax CGST RCM",
|
||||
"tax_rate": 6.00,
|
||||
"root_type": "Asset"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tax_type": {
|
||||
"account_name": "Input Tax IGST RCM",
|
||||
"tax_rate": 12.00,
|
||||
"root_type": "Asset"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "GST 28%",
|
||||
"taxes": [
|
||||
{
|
||||
"tax_type": {
|
||||
"account_name": "Output Tax SGST",
|
||||
"tax_rate": 14.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"tax_type": {
|
||||
"account_name": "Output Tax CGST",
|
||||
"tax_rate": 14.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"tax_type": {
|
||||
"account_name": "Output Tax IGST",
|
||||
"tax_rate": 28.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"tax_type": {
|
||||
"account_name": "Input Tax SGST",
|
||||
"tax_rate": 14.0,
|
||||
"root_type": "Asset"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tax_type": {
|
||||
"account_name": "Input Tax CGST",
|
||||
"tax_rate": 14.0,
|
||||
"root_type": "Asset"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tax_type": {
|
||||
"account_name": "Input Tax IGST",
|
||||
"tax_rate": 28.0,
|
||||
"root_type": "Asset"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tax_type": {
|
||||
"account_name": "Input Tax SGST RCM",
|
||||
"tax_rate": 14.00,
|
||||
"root_type": "Asset"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tax_type": {
|
||||
"account_name": "Input Tax CGST RCM",
|
||||
"tax_rate": 14.00,
|
||||
"root_type": "Asset"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tax_type": {
|
||||
"account_name": "Input Tax IGST RCM",
|
||||
"tax_rate": 28.00,
|
||||
"root_type": "Asset"
|
||||
}
|
||||
}
|
||||
]
|
||||
@ -1229,35 +1488,116 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"*": [
|
||||
"sales_tax_templates": [
|
||||
{
|
||||
"title": "In State GST",
|
||||
"title": "Output GST In-state",
|
||||
"taxes": [
|
||||
{
|
||||
"account_head": {
|
||||
"account_name": "SGST",
|
||||
"tax_rate": 9.00
|
||||
"account_name": "Output Tax SGST",
|
||||
"tax_rate": 9.00,
|
||||
"account_type": "Tax"
|
||||
}
|
||||
},
|
||||
{
|
||||
"account_head": {
|
||||
"account_name": "CGST",
|
||||
"tax_rate": 9.00
|
||||
"account_name": "Output Tax CGST",
|
||||
"tax_rate": 9.00,
|
||||
"account_type": "Tax"
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
"tax_category": "In-State"
|
||||
},
|
||||
{
|
||||
"title": "Out of State GST",
|
||||
"title": "Output GST Out-state",
|
||||
"taxes": [
|
||||
{
|
||||
"account_head": {
|
||||
"account_name": "IGST",
|
||||
"tax_rate": 18.00
|
||||
"account_name": "Output Tax IGST",
|
||||
"tax_rate": 18.00,
|
||||
"account_type": "Tax"
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
"tax_category": "Out-State"
|
||||
}
|
||||
],
|
||||
"purchase_tax_templates": [
|
||||
{
|
||||
"title": "Input GST In-state",
|
||||
"taxes": [
|
||||
{
|
||||
"account_head": {
|
||||
"account_name": "Input Tax SGST",
|
||||
"tax_rate": 9.00,
|
||||
"root_type": "Asset",
|
||||
"account_type": "Tax"
|
||||
}
|
||||
},
|
||||
{
|
||||
"account_head": {
|
||||
"account_name": "Input Tax CGST",
|
||||
"tax_rate": 9.00,
|
||||
"root_type": "Asset",
|
||||
"account_type": "Tax"
|
||||
}
|
||||
}
|
||||
],
|
||||
"tax_category": "In-State"
|
||||
},
|
||||
{
|
||||
"title": "Input GST Out-state",
|
||||
"taxes": [
|
||||
{
|
||||
"account_head": {
|
||||
"account_name": "Input Tax IGST",
|
||||
"tax_rate": 18.00,
|
||||
"root_type": "Asset",
|
||||
"account_type": "Tax"
|
||||
}
|
||||
}
|
||||
],
|
||||
"tax_category": "Out-State"
|
||||
},
|
||||
{
|
||||
"title": "Input GST RCM In-state",
|
||||
"taxes": [
|
||||
{
|
||||
"account_head": {
|
||||
"account_name": "Input Tax SGST RCM",
|
||||
"tax_rate": 9.00,
|
||||
"root_type": "Asset",
|
||||
"account_type": "Tax"
|
||||
}
|
||||
},
|
||||
{
|
||||
"account_head": {
|
||||
"account_name": "Input Tax CGST RCM",
|
||||
"tax_rate": 9.00,
|
||||
"root_type": "Asset",
|
||||
"account_type": "Tax"
|
||||
}
|
||||
}
|
||||
],
|
||||
"tax_category": "Reverse Charge In-State"
|
||||
},
|
||||
{
|
||||
"title": "Input GST RCM Out-state",
|
||||
"taxes": [
|
||||
{
|
||||
"account_head": {
|
||||
"account_name": "Input Tax IGST RCM",
|
||||
"tax_rate": 18.00,
|
||||
"root_type": "Asset",
|
||||
"account_type": "Tax"
|
||||
}
|
||||
}
|
||||
],
|
||||
"tax_category": "Reverse Charge Out-State"
|
||||
}
|
||||
],
|
||||
"*": [
|
||||
{
|
||||
"title": "VAT 5%",
|
||||
"taxes": [
|
||||
|
@ -42,29 +42,6 @@ def enable_shopping_cart(args):
|
||||
'quotation_series': "QTN-",
|
||||
}).insert()
|
||||
|
||||
def create_bank_account(args):
|
||||
if args.get("bank_account"):
|
||||
company_name = args.get('company_name')
|
||||
bank_account_group = frappe.db.get_value("Account",
|
||||
{"account_type": "Bank", "is_group": 1, "root_type": "Asset",
|
||||
"company": company_name})
|
||||
if bank_account_group:
|
||||
bank_account = frappe.get_doc({
|
||||
"doctype": "Account",
|
||||
'account_name': args.get("bank_account"),
|
||||
'parent_account': bank_account_group,
|
||||
'is_group':0,
|
||||
'company': company_name,
|
||||
"account_type": "Bank",
|
||||
})
|
||||
try:
|
||||
return bank_account.insert()
|
||||
except RootNotEditable:
|
||||
frappe.throw(_("Bank account cannot be named as {0}").format(args.get("bank_account")))
|
||||
except frappe.DuplicateEntryError:
|
||||
# bank account same as a CoA entry
|
||||
pass
|
||||
|
||||
def create_email_digest():
|
||||
from frappe.utils.user import get_system_managers
|
||||
system_managers = get_system_managers(only_name=True)
|
||||
|
@ -448,6 +448,8 @@ def install_defaults(args=None):
|
||||
set_active_domains(args)
|
||||
update_stock_settings()
|
||||
update_shopping_cart_settings(args)
|
||||
|
||||
args.update({"set_default": 1})
|
||||
create_bank_account(args)
|
||||
|
||||
def set_global_defaults(args):
|
||||
@ -479,17 +481,17 @@ def update_stock_settings():
|
||||
stock_settings.save()
|
||||
|
||||
def create_bank_account(args):
|
||||
if not args.bank_account:
|
||||
if not args.get('bank_account'):
|
||||
return
|
||||
|
||||
company_name = args.company_name
|
||||
company_name = args.get('company_name')
|
||||
bank_account_group = frappe.db.get_value("Account",
|
||||
{"account_type": "Bank", "is_group": 1, "root_type": "Asset",
|
||||
"company": company_name})
|
||||
if bank_account_group:
|
||||
bank_account = frappe.get_doc({
|
||||
"doctype": "Account",
|
||||
'account_name': args.bank_account,
|
||||
'account_name': args.get('bank_account'),
|
||||
'parent_account': bank_account_group,
|
||||
'is_group':0,
|
||||
'company': company_name,
|
||||
@ -498,10 +500,13 @@ def create_bank_account(args):
|
||||
try:
|
||||
doc = bank_account.insert()
|
||||
|
||||
frappe.db.set_value("Company", args.company_name, "default_bank_account", bank_account.name, update_modified=False)
|
||||
if args.get('set_default'):
|
||||
frappe.db.set_value("Company", args.get('company_name'), "default_bank_account", bank_account.name, update_modified=False)
|
||||
|
||||
return doc
|
||||
|
||||
except RootNotEditable:
|
||||
frappe.throw(_("Bank account cannot be named as {0}").format(args.bank_account))
|
||||
frappe.throw(_("Bank account cannot be named as {0}").format(args.get('bank_account')))
|
||||
except frappe.DuplicateEntryError:
|
||||
# bank account same as a CoA entry
|
||||
pass
|
||||
|
@ -27,6 +27,7 @@ def setup_taxes_and_charges(company_name: str, country: str):
|
||||
country_wise_tax = simple_to_detailed(country_wise_tax)
|
||||
|
||||
from_detailed_data(company_name, country_wise_tax)
|
||||
update_regional_tax_settings(country, company_name)
|
||||
|
||||
|
||||
def simple_to_detailed(templates):
|
||||
@ -86,7 +87,7 @@ def from_detailed_data(company_name, data):
|
||||
|
||||
if tax_categories:
|
||||
for tax_category in tax_categories:
|
||||
make_tax_catgory(tax_category)
|
||||
make_tax_category(tax_category)
|
||||
|
||||
if sales_tax_templates:
|
||||
for template in sales_tax_templates:
|
||||
@ -101,6 +102,17 @@ def from_detailed_data(company_name, data):
|
||||
make_item_tax_template(company_name, template)
|
||||
|
||||
|
||||
def update_regional_tax_settings(country, company):
|
||||
path = frappe.get_app_path('erpnext', 'regional', frappe.scrub(country))
|
||||
if os.path.exists(path.encode("utf-8")):
|
||||
try:
|
||||
module_name = "erpnext.regional.{0}.setup.update_regional_tax_settings".format(frappe.scrub(country))
|
||||
frappe.get_attr(module_name)(country, company)
|
||||
except Exception as e:
|
||||
# Log error and ignore if failed to setup regional tax settings
|
||||
frappe.log_error()
|
||||
pass
|
||||
|
||||
def make_taxes_and_charges_template(company_name, doctype, template):
|
||||
template['company'] = company_name
|
||||
template['doctype'] = doctype
|
||||
@ -130,8 +142,14 @@ def make_taxes_and_charges_template(company_name, doctype, template):
|
||||
if fieldname not in tax_row:
|
||||
tax_row[fieldname] = default_value
|
||||
|
||||
return frappe.get_doc(template).insert(ignore_permissions=True)
|
||||
doc = frappe.get_doc(template)
|
||||
|
||||
# Data in country wise json is already pre validated, hence validations can be ignored
|
||||
# Ingone validations to make doctypes faster
|
||||
doc.flags.ignore_links = True
|
||||
doc.flags.ignore_validate = True
|
||||
doc.insert(ignore_permissions=True)
|
||||
return doc
|
||||
|
||||
def make_item_tax_template(company_name, template):
|
||||
"""Create an Item Tax Template.
|
||||
@ -156,8 +174,14 @@ def make_item_tax_template(company_name, template):
|
||||
if 'tax_rate' not in tax_row:
|
||||
tax_row['tax_rate'] = account_data.get('tax_rate')
|
||||
|
||||
return frappe.get_doc(template).insert(ignore_permissions=True)
|
||||
doc = frappe.get_doc(template)
|
||||
|
||||
# Data in country wise json is already pre validated, hence validations can be ignored
|
||||
# Ingone validations to make doctypes faster
|
||||
doc.flags.ignore_links = True
|
||||
doc.flags.ignore_validate = True
|
||||
doc.insert(ignore_permissions=True)
|
||||
return doc
|
||||
|
||||
def get_or_create_account(company_name, account):
|
||||
"""
|
||||
@ -175,8 +199,7 @@ def get_or_create_account(company_name, account):
|
||||
or_filters={
|
||||
'account_name': account.get('account_name'),
|
||||
'account_number': account.get('account_number')
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
if existing_accounts:
|
||||
return frappe.get_doc('Account', existing_accounts[0].name)
|
||||
@ -191,8 +214,11 @@ def get_or_create_account(company_name, account):
|
||||
account['root_type'] = root_type
|
||||
account['is_group'] = 0
|
||||
|
||||
return frappe.get_doc(account).insert(ignore_permissions=True, ignore_mandatory=True)
|
||||
|
||||
doc = frappe.get_doc(account)
|
||||
doc.flags.ignore_links = True
|
||||
doc.flags.ignore_validate = True
|
||||
doc.insert(ignore_permissions=True, ignore_mandatory=True)
|
||||
return doc
|
||||
|
||||
def get_or_create_tax_group(company_name, root_type):
|
||||
# Look for a group account of type 'Tax'
|
||||
@ -237,14 +263,18 @@ def get_or_create_tax_group(company_name, root_type):
|
||||
'account_type': 'Tax',
|
||||
'account_name': account_name,
|
||||
'parent_account': root_account.name
|
||||
}).insert(ignore_permissions=True)
|
||||
})
|
||||
|
||||
tax_group_account.flags.ignore_links = True
|
||||
tax_group_account.flags.ignore_validate = True
|
||||
tax_group_account.insert(ignore_permissions=True)
|
||||
|
||||
tax_group_name = tax_group_account.name
|
||||
|
||||
return tax_group_name
|
||||
|
||||
|
||||
def make_tax_catgory(tax_category):
|
||||
def make_tax_category(tax_category):
|
||||
doctype = 'Tax Category'
|
||||
if isinstance(tax_category, str):
|
||||
tax_category = {'title': tax_category}
|
||||
|
@ -193,7 +193,7 @@
|
||||
"image_field": "image",
|
||||
"links": [],
|
||||
"max_attachments": 5,
|
||||
"modified": "2021-01-07 11:10:09.149170",
|
||||
"modified": "2021-07-08 16:22:01.343105",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Batch",
|
||||
@ -217,5 +217,6 @@
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"title_field": "batch_id"
|
||||
"title_field": "batch_id",
|
||||
"track_changes": 1
|
||||
}
|
@ -587,8 +587,8 @@ def make_item_variant():
|
||||
test_records = frappe.get_test_records('Item')
|
||||
|
||||
def create_item(item_code, is_stock_item=1, valuation_rate=0, warehouse="_Test Warehouse - _TC",
|
||||
is_customer_provided_item=None, customer=None, is_purchase_item=None, opening_stock=0,
|
||||
company="_Test Company"):
|
||||
is_customer_provided_item=None, customer=None, is_purchase_item=None, opening_stock=0, is_fixed_asset=0,
|
||||
asset_category=None, company="_Test Company"):
|
||||
if not frappe.db.exists("Item", item_code):
|
||||
item = frappe.new_doc("Item")
|
||||
item.item_code = item_code
|
||||
@ -596,6 +596,8 @@ def create_item(item_code, is_stock_item=1, valuation_rate=0, warehouse="_Test W
|
||||
item.description = item_code
|
||||
item.item_group = "All Item Groups"
|
||||
item.is_stock_item = is_stock_item
|
||||
item.is_fixed_asset = is_fixed_asset
|
||||
item.asset_category = asset_category
|
||||
item.opening_stock = opening_stock
|
||||
item.valuation_rate = valuation_rate
|
||||
item.is_purchase_item = is_purchase_item
|
||||
|
@ -133,6 +133,6 @@ def repost_entries():
|
||||
|
||||
def get_repost_item_valuation_entries():
|
||||
return frappe.db.sql(""" SELECT name from `tabRepost Item Valuation`
|
||||
WHERE status != 'Completed' and creation <= %s and docstatus = 1
|
||||
WHERE status in ('Queued', 'In Progress') and creation <= %s and docstatus = 1
|
||||
ORDER BY timestamp(posting_date, posting_time) asc, creation asc
|
||||
""", now(), as_dict=1)
|
||||
|
@ -72,7 +72,7 @@ class StockEntry(StockController):
|
||||
self.validate_with_material_request()
|
||||
self.validate_batch()
|
||||
self.validate_inspection()
|
||||
self.validate_fg_completed_qty()
|
||||
# self.validate_fg_completed_qty()
|
||||
self.validate_difference_account()
|
||||
self.set_job_card_data()
|
||||
self.set_purpose_for_stock_entry()
|
||||
@ -719,6 +719,10 @@ class StockEntry(StockController):
|
||||
frappe.throw(_("Multiple items cannot be marked as finished item"))
|
||||
|
||||
if self.purpose == "Manufacture":
|
||||
if not finished_items:
|
||||
frappe.throw(_('Finished Good has not set in the stock entry {0}')
|
||||
.format(self.name))
|
||||
|
||||
allowance_percentage = flt(frappe.db.get_single_value("Manufacturing Settings",
|
||||
"overproduction_percentage_for_work_order"))
|
||||
|
||||
@ -1090,13 +1094,13 @@ class StockEntry(StockController):
|
||||
"is_finished_item": 1
|
||||
}
|
||||
|
||||
if self.work_order and self.pro_doc.has_batch_no:
|
||||
if self.work_order and self.pro_doc.has_batch_no and cint(frappe.db.get_single_value('Manufacturing Settings',
|
||||
'make_serial_no_batch_from_work_order', cache=True)):
|
||||
self.set_batchwise_finished_goods(args, item)
|
||||
else:
|
||||
self.add_finisged_goods(args, item)
|
||||
self.add_finished_goods(args, item)
|
||||
|
||||
def set_batchwise_finished_goods(self, args, item):
|
||||
qty = flt(self.fg_completed_qty)
|
||||
filters = {
|
||||
"reference_name": self.pro_doc.name,
|
||||
"reference_doctype": self.pro_doc.doctype,
|
||||
@ -1105,7 +1109,17 @@ class StockEntry(StockController):
|
||||
|
||||
fields = ["qty_to_produce as qty", "produced_qty", "name"]
|
||||
|
||||
for row in frappe.get_all("Batch", filters = filters, fields = fields, order_by="creation asc"):
|
||||
data = frappe.get_all("Batch", filters = filters, fields = fields, order_by="creation asc")
|
||||
|
||||
if not data:
|
||||
self.add_finished_goods(args, item)
|
||||
else:
|
||||
self.add_batchwise_finished_good(data, args, item)
|
||||
|
||||
def add_batchwise_finished_good(self, data, args, item):
|
||||
qty = flt(self.fg_completed_qty)
|
||||
|
||||
for row in data:
|
||||
batch_qty = flt(row.qty) - flt(row.produced_qty)
|
||||
if not batch_qty:
|
||||
continue
|
||||
@ -1121,9 +1135,9 @@ class StockEntry(StockController):
|
||||
args["qty"] = fg_qty
|
||||
args["batch_no"] = row.name
|
||||
|
||||
self.add_finisged_goods(args, item)
|
||||
self.add_finished_goods(args, item)
|
||||
|
||||
def add_finisged_goods(self, args, item):
|
||||
def add_finished_goods(self, args, item):
|
||||
self.add_to_stock_entry_detail({
|
||||
item.name: args
|
||||
}, bom_no = self.bom_no)
|
||||
|
@ -89,17 +89,16 @@ class StockLedgerEntry(Document):
|
||||
if item_det.is_stock_item != 1:
|
||||
frappe.throw(_("Item {0} must be a stock Item").format(self.item_code))
|
||||
|
||||
# check if batch number is required
|
||||
if self.voucher_type != 'Stock Reconciliation':
|
||||
if item_det.has_batch_no == 1:
|
||||
batch_item = self.item_code if self.item_code == item_det.item_name else self.item_code + ":" + item_det.item_name
|
||||
if not self.batch_no:
|
||||
frappe.throw(_("Batch number is mandatory for Item {0}").format(batch_item))
|
||||
elif not frappe.db.get_value("Batch",{"item": self.item_code, "name": self.batch_no}):
|
||||
frappe.throw(_("{0} is not a valid Batch Number for Item {1}").format(self.batch_no, batch_item))
|
||||
# check if batch number is valid
|
||||
if item_det.has_batch_no == 1:
|
||||
batch_item = self.item_code if self.item_code == item_det.item_name else self.item_code + ":" + item_det.item_name
|
||||
if not self.batch_no:
|
||||
frappe.throw(_("Batch number is mandatory for Item {0}").format(batch_item))
|
||||
elif not frappe.db.get_value("Batch",{"item": self.item_code, "name": self.batch_no}):
|
||||
frappe.throw(_("{0} is not a valid Batch Number for Item {1}").format(self.batch_no, batch_item))
|
||||
|
||||
elif item_det.has_batch_no == 0 and self.batch_no and self.is_cancelled == 0:
|
||||
frappe.throw(_("The Item {0} cannot have Batch").format(self.item_code))
|
||||
elif item_det.has_batch_no == 0 and self.batch_no and self.is_cancelled == 0:
|
||||
frappe.throw(_("The Item {0} cannot have Batch").format(self.item_code))
|
||||
|
||||
if item_det.has_variants:
|
||||
frappe.throw(_("Stock cannot exist for Item {0} since has variants").format(self.item_code),
|
||||
|
@ -17,6 +17,14 @@ frappe.ui.form.on("Stock Reconciliation", {
|
||||
}
|
||||
}
|
||||
});
|
||||
frm.set_query("batch_no", "items", function(doc, cdt, cdn) {
|
||||
var item = locals[cdt][cdn];
|
||||
return {
|
||||
filters: {
|
||||
'item': item.item_code
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
if (frm.doc.company) {
|
||||
erpnext.queries.setup_queries(frm, "Warehouse", function() {
|
||||
|
@ -405,17 +405,18 @@ class StockReconciliation(StockController):
|
||||
|
||||
key = (d.item_code, d.warehouse)
|
||||
if key not in merge_similar_entries:
|
||||
d.total_amount = (d.actual_qty * d.valuation_rate)
|
||||
merge_similar_entries[key] = d
|
||||
elif d.serial_no:
|
||||
data = merge_similar_entries[key]
|
||||
data.actual_qty += d.actual_qty
|
||||
data.qty_after_transaction += d.qty_after_transaction
|
||||
|
||||
data.valuation_rate = (data.valuation_rate + d.valuation_rate) / data.actual_qty
|
||||
data.total_amount += (d.actual_qty * d.valuation_rate)
|
||||
data.valuation_rate = (data.total_amount) / data.actual_qty
|
||||
data.serial_no += '\n' + d.serial_no
|
||||
|
||||
if data.incoming_rate:
|
||||
data.incoming_rate = (data.incoming_rate + d.incoming_rate) / data.actual_qty
|
||||
data.incoming_rate = (data.total_amount) / data.actual_qty
|
||||
|
||||
for key, value in merge_similar_entries.items():
|
||||
new_sl_entries.append(value)
|
||||
|
@ -6,7 +6,7 @@
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import frappe, unittest
|
||||
from frappe.utils import flt, nowdate, nowtime, add_days
|
||||
from frappe.utils import flt, nowdate, nowtime, random_string, add_days
|
||||
from erpnext.accounts.utils import get_stock_and_account_balance
|
||||
from erpnext.stock.stock_ledger import get_previous_sle, update_entries_after
|
||||
from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import EmptyStockReconciliationItemsError, get_items
|
||||
@ -16,6 +16,7 @@ from erpnext.stock.utils import get_incoming_rate, get_stock_value_on, get_valua
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
|
||||
|
||||
|
||||
class TestStockReconciliation(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(self):
|
||||
@ -151,6 +152,42 @@ class TestStockReconciliation(unittest.TestCase):
|
||||
stock_doc = frappe.get_doc("Stock Reconciliation", d)
|
||||
stock_doc.cancel()
|
||||
|
||||
|
||||
def test_stock_reco_for_merge_serialized_item(self):
|
||||
to_delete_records = []
|
||||
|
||||
# Add new serial nos
|
||||
serial_item_code = "Stock-Reco-Serial-Item-2"
|
||||
serial_warehouse = "_Test Warehouse for Stock Reco1 - _TC"
|
||||
|
||||
sr = create_stock_reconciliation(item_code=serial_item_code, serial_no=random_string(6),
|
||||
warehouse = serial_warehouse, qty=1, rate=100, do_not_submit=True, purpose='Opening Stock')
|
||||
|
||||
for i in range(3):
|
||||
sr.append('items', {
|
||||
'item_code': serial_item_code,
|
||||
'warehouse': serial_warehouse,
|
||||
'qty': 1,
|
||||
'valuation_rate': 100,
|
||||
'serial_no': random_string(6)
|
||||
})
|
||||
|
||||
sr.save()
|
||||
sr.submit()
|
||||
|
||||
sle_entries = frappe.get_all('Stock Ledger Entry', filters= {'voucher_no': sr.name},
|
||||
fields = ['name', 'incoming_rate'])
|
||||
|
||||
self.assertEqual(len(sle_entries), 1)
|
||||
self.assertEqual(sle_entries[0].incoming_rate, 100)
|
||||
|
||||
to_delete_records.append(sr.name)
|
||||
to_delete_records.reverse()
|
||||
|
||||
for d in to_delete_records:
|
||||
stock_doc = frappe.get_doc("Stock Reconciliation", d)
|
||||
stock_doc.cancel()
|
||||
|
||||
def test_stock_reco_for_batch_item(self):
|
||||
to_delete_records = []
|
||||
to_delete_serial_nos = []
|
||||
@ -316,6 +353,26 @@ class TestStockReconciliation(unittest.TestCase):
|
||||
dn2.cancel()
|
||||
pr1.cancel()
|
||||
|
||||
def test_valid_batch(self):
|
||||
create_batch_item_with_batch("Testing Batch Item 1", "001")
|
||||
create_batch_item_with_batch("Testing Batch Item 2", "002")
|
||||
sr = create_stock_reconciliation(item_code="Testing Batch Item 1", qty=1, rate=100, batch_no="002"
|
||||
, do_not_submit=True)
|
||||
self.assertRaises(frappe.ValidationError, sr.submit)
|
||||
|
||||
def create_batch_item_with_batch(item_name, batch_id):
|
||||
batch_item_doc = create_item(item_name, is_stock_item=1)
|
||||
if not batch_item_doc.has_batch_no:
|
||||
batch_item_doc.has_batch_no = 1
|
||||
batch_item_doc.create_new_batch = 1
|
||||
batch_item_doc.save(ignore_permissions=True)
|
||||
|
||||
if not frappe.db.exists('Batch', batch_id):
|
||||
b = frappe.new_doc('Batch')
|
||||
b.item = item_name
|
||||
b.batch_id = batch_id
|
||||
b.save()
|
||||
|
||||
def insert_existing_sle(warehouse):
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||
|
||||
@ -343,6 +400,12 @@ def create_batch_or_serial_no_items():
|
||||
serial_item_doc.serial_no_series = "SRSI.####"
|
||||
serial_item_doc.save(ignore_permissions=True)
|
||||
|
||||
serial_item_doc = create_item("Stock-Reco-Serial-Item-2", is_stock_item=1)
|
||||
if not serial_item_doc.has_serial_no:
|
||||
serial_item_doc.has_serial_no = 1
|
||||
serial_item_doc.serial_no_series = "SRSII.####"
|
||||
serial_item_doc.save(ignore_permissions=True)
|
||||
|
||||
batch_item_doc = create_item("Stock-Reco-batch-Item-1", is_stock_item=1)
|
||||
if not batch_item_doc.has_batch_no:
|
||||
batch_item_doc.has_batch_no = 1
|
||||
|
@ -807,10 +807,14 @@ def check_packing_list(price_list_rate_name, desired_qty, item_code):
|
||||
def validate_conversion_rate(args, meta):
|
||||
from erpnext.controllers.accounts_controller import validate_conversion_rate
|
||||
|
||||
if (not args.conversion_rate
|
||||
and args.currency==frappe.get_cached_value('Company', args.company, "default_currency")):
|
||||
company_currency = frappe.get_cached_value('Company', args.company, "default_currency")
|
||||
if (not args.conversion_rate and args.currency==company_currency):
|
||||
args.conversion_rate = 1.0
|
||||
|
||||
if (not args.ignore_conversion_rate and args.conversion_rate == 1 and args.currency!=company_currency):
|
||||
args.conversion_rate = get_exchange_rate(args.currency,
|
||||
company_currency, args.transaction_date, "for_buying") or 1.0
|
||||
|
||||
# validate currency conversion rate
|
||||
validate_conversion_rate(args.currency, args.conversion_rate,
|
||||
meta.get_label("conversion_rate"), args.company)
|
||||
|
@ -314,13 +314,16 @@ def update_included_uom_in_report(columns, result, include_uom, conversion_facto
|
||||
for row_idx, row in enumerate(result):
|
||||
data = row.items() if is_dict_obj else enumerate(row)
|
||||
for key, value in data:
|
||||
if key not in convertible_columns or not conversion_factors[row_idx-1]:
|
||||
if key not in convertible_columns:
|
||||
continue
|
||||
# If no conversion factor for the UOM, defaults to 1
|
||||
if not conversion_factors[row_idx]:
|
||||
conversion_factors[row_idx] = 1
|
||||
|
||||
if convertible_columns.get(key) == 'rate':
|
||||
new_value = flt(value) * conversion_factors[row_idx-1]
|
||||
new_value = flt(value) * conversion_factors[row_idx]
|
||||
else:
|
||||
new_value = flt(value) / conversion_factors[row_idx-1]
|
||||
new_value = flt(value) / conversion_factors[row_idx]
|
||||
|
||||
if not is_dict_obj:
|
||||
row.insert(key+1, new_value)
|
||||
|
@ -5,10 +5,10 @@ from __future__ import unicode_literals
|
||||
import frappe
|
||||
import json
|
||||
from frappe import _
|
||||
from frappe import utils
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import now_datetime
|
||||
from datetime import datetime, timedelta
|
||||
from frappe.utils import now_datetime, time_diff_in_seconds, get_datetime, date_diff
|
||||
from frappe.core.utils import get_parent_doc
|
||||
from datetime import timedelta
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
from frappe.utils.user import is_website_user
|
||||
from frappe.email.inbox import link_communication_to_document
|
||||
@ -212,6 +212,128 @@ def make_issue_from_communication(communication, ignore_communication_links=Fals
|
||||
|
||||
return issue.name
|
||||
|
||||
def get_time_in_timedelta(time):
|
||||
"""
|
||||
Converts datetime.time(10, 36, 55, 961454) to datetime.timedelta(seconds=38215)
|
||||
"""
|
||||
return timedelta(hours=time.hour, minutes=time.minute, seconds=time.second)
|
||||
|
||||
def set_first_response_time(communication, method):
|
||||
if communication.get('reference_doctype') == "Issue":
|
||||
issue = get_parent_doc(communication)
|
||||
if is_first_response(issue):
|
||||
first_response_time = calculate_first_response_time(issue, get_datetime(issue.first_responded_on))
|
||||
issue.db_set("first_response_time", first_response_time)
|
||||
|
||||
def is_first_response(issue):
|
||||
responses = frappe.get_all('Communication', filters = {'reference_name': issue.name, 'sent_or_received': 'Sent'})
|
||||
if len(responses) == 1:
|
||||
return True
|
||||
return False
|
||||
|
||||
def calculate_first_response_time(issue, first_responded_on):
|
||||
issue_creation_date = issue.creation
|
||||
issue_creation_time = get_time_in_seconds(issue_creation_date)
|
||||
first_responded_on_in_seconds = get_time_in_seconds(first_responded_on)
|
||||
support_hours = frappe.get_cached_doc("Service Level Agreement", issue.service_level_agreement).support_and_resolution
|
||||
|
||||
if issue_creation_date.day == first_responded_on.day:
|
||||
if is_work_day(issue_creation_date, support_hours):
|
||||
start_time, end_time = get_working_hours(issue_creation_date, support_hours)
|
||||
|
||||
# issue creation and response on the same day during working hours
|
||||
if is_during_working_hours(issue_creation_date, support_hours) and is_during_working_hours(first_responded_on, support_hours):
|
||||
return get_elapsed_time(issue_creation_date, first_responded_on)
|
||||
|
||||
# issue creation is during working hours, but first response was after working hours
|
||||
elif is_during_working_hours(issue_creation_date, support_hours):
|
||||
return get_elapsed_time(issue_creation_time, end_time)
|
||||
|
||||
# issue creation was before working hours but first response is during working hours
|
||||
elif is_during_working_hours(first_responded_on, support_hours):
|
||||
return get_elapsed_time(start_time, first_responded_on_in_seconds)
|
||||
|
||||
# both issue creation and first response were after working hours
|
||||
else:
|
||||
return 1.0 # this should ideally be zero, but it gets reset when the next response is sent if the value is zero
|
||||
|
||||
else:
|
||||
return 1.0
|
||||
|
||||
else:
|
||||
# response on the next day
|
||||
if date_diff(first_responded_on, issue_creation_date) == 1:
|
||||
first_response_time = 0
|
||||
else:
|
||||
first_response_time = calculate_initial_frt(issue_creation_date, date_diff(first_responded_on, issue_creation_date)- 1, support_hours)
|
||||
|
||||
# time taken on day of issue creation
|
||||
if is_work_day(issue_creation_date, support_hours):
|
||||
start_time, end_time = get_working_hours(issue_creation_date, support_hours)
|
||||
|
||||
if is_during_working_hours(issue_creation_date, support_hours):
|
||||
first_response_time += get_elapsed_time(issue_creation_time, end_time)
|
||||
elif is_before_working_hours(issue_creation_date, support_hours):
|
||||
first_response_time += get_elapsed_time(start_time, end_time)
|
||||
|
||||
# time taken on day of first response
|
||||
if is_work_day(first_responded_on, support_hours):
|
||||
start_time, end_time = get_working_hours(first_responded_on, support_hours)
|
||||
|
||||
if is_during_working_hours(first_responded_on, support_hours):
|
||||
first_response_time += get_elapsed_time(start_time, first_responded_on_in_seconds)
|
||||
elif not is_before_working_hours(first_responded_on, support_hours):
|
||||
first_response_time += get_elapsed_time(start_time, end_time)
|
||||
|
||||
if first_response_time:
|
||||
return first_response_time
|
||||
else:
|
||||
return 1.0
|
||||
|
||||
def get_time_in_seconds(date):
|
||||
return timedelta(hours=date.hour, minutes=date.minute, seconds=date.second)
|
||||
|
||||
def get_working_hours(date, support_hours):
|
||||
if is_work_day(date, support_hours):
|
||||
weekday = frappe.utils.get_weekday(date)
|
||||
for day in support_hours:
|
||||
if day.workday == weekday:
|
||||
return day.start_time, day.end_time
|
||||
|
||||
def is_work_day(date, support_hours):
|
||||
weekday = frappe.utils.get_weekday(date)
|
||||
for day in support_hours:
|
||||
if day.workday == weekday:
|
||||
return True
|
||||
return False
|
||||
|
||||
def is_during_working_hours(date, support_hours):
|
||||
start_time, end_time = get_working_hours(date, support_hours)
|
||||
time = get_time_in_seconds(date)
|
||||
if time >= start_time and time <= end_time:
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_elapsed_time(start_time, end_time):
|
||||
return round(time_diff_in_seconds(end_time, start_time), 2)
|
||||
|
||||
def calculate_initial_frt(issue_creation_date, days_in_between, support_hours):
|
||||
initial_frt = 0
|
||||
for i in range(days_in_between):
|
||||
date = issue_creation_date + timedelta(days = (i+1))
|
||||
if is_work_day(date, support_hours):
|
||||
start_time, end_time = get_working_hours(date, support_hours)
|
||||
initial_frt += get_elapsed_time(start_time, end_time)
|
||||
|
||||
return initial_frt
|
||||
|
||||
def is_before_working_hours(date, support_hours):
|
||||
start_time, end_time = get_working_hours(date, support_hours)
|
||||
time = get_time_in_seconds(date)
|
||||
if time < start_time:
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_holidays(holiday_list_name):
|
||||
holiday_list = frappe.get_cached_doc("Holiday List", holiday_list_name)
|
||||
holidays = [holiday.holiday_date for holiday in holiday_list.holidays]
|
||||
|
@ -5,16 +5,18 @@ from __future__ import unicode_literals
|
||||
import frappe
|
||||
import unittest
|
||||
from erpnext.support.doctype.service_level_agreement.test_service_level_agreement import create_service_level_agreements_for_issues
|
||||
from frappe.utils import now_datetime, get_datetime, flt
|
||||
from frappe.core.doctype.user_permission.test_user_permission import create_user
|
||||
from frappe.utils import get_datetime, flt
|
||||
import datetime
|
||||
from datetime import timedelta
|
||||
|
||||
class TestIssue(unittest.TestCase):
|
||||
class TestSetUp(unittest.TestCase):
|
||||
def setUp(self):
|
||||
frappe.db.sql("delete from `tabService Level Agreement`")
|
||||
frappe.db.set_value("Support Settings", None, "track_service_level_agreement", 1)
|
||||
create_service_level_agreements_for_issues()
|
||||
|
||||
class TestIssue(TestSetUp):
|
||||
def test_response_time_and_resolution_time_based_on_different_sla(self):
|
||||
creation = datetime.datetime(2019, 3, 4, 12, 0)
|
||||
|
||||
@ -133,6 +135,223 @@ class TestIssue(unittest.TestCase):
|
||||
issue.reload()
|
||||
self.assertEqual(flt(issue.total_hold_time, 2), 2700)
|
||||
|
||||
class TestFirstResponseTime(TestSetUp):
|
||||
# working hours used in all cases: Mon-Fri, 10am to 6pm
|
||||
# all dates are in the mm-dd-yyyy format
|
||||
|
||||
# issue creation and first response are on the same day
|
||||
def test_first_response_time_case1(self):
|
||||
"""
|
||||
Test frt when issue creation and first response are during working hours on the same day.
|
||||
"""
|
||||
issue = create_issue_and_communication(get_datetime("06-28-2021 11:00"), get_datetime("06-28-2021 12:00"))
|
||||
self.assertEqual(issue.first_response_time, 3600.0)
|
||||
|
||||
def test_first_response_time_case2(self):
|
||||
"""
|
||||
Test frt when issue creation was during working hours, but first response is sent after working hours on the same day.
|
||||
"""
|
||||
issue = create_issue_and_communication(get_datetime("06-28-2021 12:00"), get_datetime("06-28-2021 20:00"))
|
||||
self.assertEqual(issue.first_response_time, 21600.0)
|
||||
|
||||
def test_first_response_time_case3(self):
|
||||
"""
|
||||
Test frt when issue creation was before working hours but first response is sent during working hours on the same day.
|
||||
"""
|
||||
issue = create_issue_and_communication(get_datetime("06-28-2021 6:00"), get_datetime("06-28-2021 12:00"))
|
||||
self.assertEqual(issue.first_response_time, 7200.0)
|
||||
|
||||
def test_first_response_time_case4(self):
|
||||
"""
|
||||
Test frt when both issue creation and first response were after working hours on the same day.
|
||||
"""
|
||||
issue = create_issue_and_communication(get_datetime("06-28-2021 19:00"), get_datetime("06-28-2021 20:00"))
|
||||
self.assertEqual(issue.first_response_time, 1.0)
|
||||
|
||||
def test_first_response_time_case5(self):
|
||||
"""
|
||||
Test frt when both issue creation and first response are on the same day, but it's not a work day.
|
||||
"""
|
||||
issue = create_issue_and_communication(get_datetime("06-27-2021 10:00"), get_datetime("06-27-2021 11:00"))
|
||||
self.assertEqual(issue.first_response_time, 1.0)
|
||||
|
||||
# issue creation and first response are on consecutive days
|
||||
def test_first_response_time_case6(self):
|
||||
"""
|
||||
Test frt when the issue was created before working hours and the first response is also sent before working hours, but on the next day.
|
||||
"""
|
||||
issue = create_issue_and_communication(get_datetime("06-28-2021 6:00"), get_datetime("06-29-2021 6:00"))
|
||||
self.assertEqual(issue.first_response_time, 28800.0)
|
||||
|
||||
def test_first_response_time_case7(self):
|
||||
"""
|
||||
Test frt when the issue was created before working hours and the first response is sent during working hours, but on the next day.
|
||||
"""
|
||||
issue = create_issue_and_communication(get_datetime("06-28-2021 6:00"), get_datetime("06-29-2021 11:00"))
|
||||
self.assertEqual(issue.first_response_time, 32400.0)
|
||||
|
||||
def test_first_response_time_case8(self):
|
||||
"""
|
||||
Test frt when the issue was created before working hours and the first response is sent after working hours, but on the next day.
|
||||
"""
|
||||
issue = create_issue_and_communication(get_datetime("06-28-2021 6:00"), get_datetime("06-29-2021 20:00"))
|
||||
self.assertEqual(issue.first_response_time, 57600.0)
|
||||
|
||||
def test_first_response_time_case9(self):
|
||||
"""
|
||||
Test frt when the issue was created before working hours and the first response is sent on the next day, which is not a work day.
|
||||
"""
|
||||
issue = create_issue_and_communication(get_datetime("06-25-2021 6:00"), get_datetime("06-26-2021 11:00"))
|
||||
self.assertEqual(issue.first_response_time, 28800.0)
|
||||
|
||||
def test_first_response_time_case10(self):
|
||||
"""
|
||||
Test frt when the issue was created during working hours and the first response is sent before working hours, but on the next day.
|
||||
"""
|
||||
issue = create_issue_and_communication(get_datetime("06-28-2021 12:00"), get_datetime("06-29-2021 6:00"))
|
||||
self.assertEqual(issue.first_response_time, 21600.0)
|
||||
|
||||
def test_first_response_time_case11(self):
|
||||
"""
|
||||
Test frt when the issue was created during working hours and the first response is also sent during working hours, but on the next day.
|
||||
"""
|
||||
issue = create_issue_and_communication(get_datetime("06-28-2021 12:00"), get_datetime("06-29-2021 11:00"))
|
||||
self.assertEqual(issue.first_response_time, 25200.0)
|
||||
|
||||
def test_first_response_time_case12(self):
|
||||
"""
|
||||
Test frt when the issue was created during working hours and the first response is sent after working hours, but on the next day.
|
||||
"""
|
||||
issue = create_issue_and_communication(get_datetime("06-28-2021 12:00"), get_datetime("06-29-2021 20:00"))
|
||||
self.assertEqual(issue.first_response_time, 50400.0)
|
||||
|
||||
def test_first_response_time_case13(self):
|
||||
"""
|
||||
Test frt when the issue was created during working hours and the first response is sent on the next day, which is not a work day.
|
||||
"""
|
||||
issue = create_issue_and_communication(get_datetime("06-25-2021 12:00"), get_datetime("06-26-2021 11:00"))
|
||||
self.assertEqual(issue.first_response_time, 21600.0)
|
||||
|
||||
def test_first_response_time_case14(self):
|
||||
"""
|
||||
Test frt when the issue was created after working hours and the first response is sent before working hours, but on the next day.
|
||||
"""
|
||||
issue = create_issue_and_communication(get_datetime("06-28-2021 20:00"), get_datetime("06-29-2021 6:00"))
|
||||
self.assertEqual(issue.first_response_time, 1.0)
|
||||
|
||||
def test_first_response_time_case15(self):
|
||||
"""
|
||||
Test frt when the issue was created after working hours and the first response is sent during working hours, but on the next day.
|
||||
"""
|
||||
issue = create_issue_and_communication(get_datetime("06-28-2021 20:00"), get_datetime("06-29-2021 11:00"))
|
||||
self.assertEqual(issue.first_response_time, 3600.0)
|
||||
|
||||
def test_first_response_time_case16(self):
|
||||
"""
|
||||
Test frt when the issue was created after working hours and the first response is also sent after working hours, but on the next day.
|
||||
"""
|
||||
issue = create_issue_and_communication(get_datetime("06-28-2021 20:00"), get_datetime("06-29-2021 20:00"))
|
||||
self.assertEqual(issue.first_response_time, 28800.0)
|
||||
|
||||
def test_first_response_time_case17(self):
|
||||
"""
|
||||
Test frt when the issue was created after working hours and the first response is sent on the next day, which is not a work day.
|
||||
"""
|
||||
issue = create_issue_and_communication(get_datetime("06-25-2021 20:00"), get_datetime("06-26-2021 11:00"))
|
||||
self.assertEqual(issue.first_response_time, 1.0)
|
||||
|
||||
# issue creation and first response are a few days apart
|
||||
def test_first_response_time_case18(self):
|
||||
"""
|
||||
Test frt when the issue was created before working hours and the first response is also sent before working hours, but after a few days.
|
||||
"""
|
||||
issue = create_issue_and_communication(get_datetime("06-28-2021 6:00"), get_datetime("07-01-2021 6:00"))
|
||||
self.assertEqual(issue.first_response_time, 86400.0)
|
||||
|
||||
def test_first_response_time_case19(self):
|
||||
"""
|
||||
Test frt when the issue was created before working hours and the first response is sent during working hours, but after a few days.
|
||||
"""
|
||||
issue = create_issue_and_communication(get_datetime("06-28-2021 6:00"), get_datetime("07-01-2021 11:00"))
|
||||
self.assertEqual(issue.first_response_time, 90000.0)
|
||||
|
||||
def test_first_response_time_case20(self):
|
||||
"""
|
||||
Test frt when the issue was created before working hours and the first response is sent after working hours, but after a few days.
|
||||
"""
|
||||
issue = create_issue_and_communication(get_datetime("06-28-2021 6:00"), get_datetime("07-01-2021 20:00"))
|
||||
self.assertEqual(issue.first_response_time, 115200.0)
|
||||
|
||||
def test_first_response_time_case21(self):
|
||||
"""
|
||||
Test frt when the issue was created before working hours and the first response is sent after a few days, on a holiday.
|
||||
"""
|
||||
issue = create_issue_and_communication(get_datetime("06-25-2021 6:00"), get_datetime("06-27-2021 11:00"))
|
||||
self.assertEqual(issue.first_response_time, 28800.0)
|
||||
|
||||
def test_first_response_time_case22(self):
|
||||
"""
|
||||
Test frt when the issue was created during working hours and the first response is sent before working hours, but after a few days.
|
||||
"""
|
||||
issue = create_issue_and_communication(get_datetime("06-28-2021 12:00"), get_datetime("07-01-2021 6:00"))
|
||||
self.assertEqual(issue.first_response_time, 79200.0)
|
||||
|
||||
def test_first_response_time_case23(self):
|
||||
"""
|
||||
Test frt when the issue was created during working hours and the first response is also sent during working hours, but after a few days.
|
||||
"""
|
||||
issue = create_issue_and_communication(get_datetime("06-28-2021 12:00"), get_datetime("07-01-2021 11:00"))
|
||||
self.assertEqual(issue.first_response_time, 82800.0)
|
||||
|
||||
def test_first_response_time_case24(self):
|
||||
"""
|
||||
Test frt when the issue was created during working hours and the first response is sent after working hours, but after a few days.
|
||||
"""
|
||||
issue = create_issue_and_communication(get_datetime("06-28-2021 12:00"), get_datetime("07-01-2021 20:00"))
|
||||
self.assertEqual(issue.first_response_time, 108000.0)
|
||||
|
||||
def test_first_response_time_case25(self):
|
||||
"""
|
||||
Test frt when the issue was created during working hours and the first response is sent after a few days, on a holiday.
|
||||
"""
|
||||
issue = create_issue_and_communication(get_datetime("06-25-2021 12:00"), get_datetime("06-27-2021 11:00"))
|
||||
self.assertEqual(issue.first_response_time, 21600.0)
|
||||
|
||||
def test_first_response_time_case26(self):
|
||||
"""
|
||||
Test frt when the issue was created after working hours and the first response is sent before working hours, but after a few days.
|
||||
"""
|
||||
issue = create_issue_and_communication(get_datetime("06-28-2021 20:00"), get_datetime("07-01-2021 6:00"))
|
||||
self.assertEqual(issue.first_response_time, 57600.0)
|
||||
|
||||
def test_first_response_time_case27(self):
|
||||
"""
|
||||
Test frt when the issue was created after working hours and the first response is sent during working hours, but after a few days.
|
||||
"""
|
||||
issue = create_issue_and_communication(get_datetime("06-28-2021 20:00"), get_datetime("07-01-2021 11:00"))
|
||||
self.assertEqual(issue.first_response_time, 61200.0)
|
||||
|
||||
def test_first_response_time_case28(self):
|
||||
"""
|
||||
Test frt when the issue was created after working hours and the first response is also sent after working hours, but after a few days.
|
||||
"""
|
||||
issue = create_issue_and_communication(get_datetime("06-28-2021 20:00"), get_datetime("07-01-2021 20:00"))
|
||||
self.assertEqual(issue.first_response_time, 86400.0)
|
||||
|
||||
def test_first_response_time_case29(self):
|
||||
"""
|
||||
Test frt when the issue was created after working hours and the first response is sent after a few days, on a holiday.
|
||||
"""
|
||||
issue = create_issue_and_communication(get_datetime("06-25-2021 20:00"), get_datetime("06-27-2021 11:00"))
|
||||
self.assertEqual(issue.first_response_time, 1.0)
|
||||
|
||||
def create_issue_and_communication(issue_creation, first_responded_on):
|
||||
issue = make_issue(issue_creation, index=1)
|
||||
sender = create_user("test@admin.com")
|
||||
create_communication(issue.name, sender.email, "Sent", first_responded_on)
|
||||
issue.reload()
|
||||
|
||||
return issue
|
||||
|
||||
def make_issue(creation=None, customer=None, index=0, priority=None, issue_type=None):
|
||||
issue = frappe.get_doc({
|
||||
@ -185,7 +404,7 @@ def create_territory(territory):
|
||||
|
||||
|
||||
def create_communication(reference_name, sender, sent_or_received, creation):
|
||||
issue = frappe.get_doc({
|
||||
communication = frappe.get_doc({
|
||||
"doctype": "Communication",
|
||||
"communication_type": "Communication",
|
||||
"communication_medium": "Email",
|
||||
@ -199,4 +418,4 @@ def create_communication(reference_name, sender, sent_or_received, creation):
|
||||
"creation": creation,
|
||||
"reference_name": reference_name
|
||||
})
|
||||
issue.save()
|
||||
communication.save()
|
@ -339,16 +339,6 @@ def create_service_level_agreement(default_service_level_agreement, holiday_list
|
||||
"workday": "Friday",
|
||||
"start_time": "10:00:00",
|
||||
"end_time": "18:00:00",
|
||||
},
|
||||
{
|
||||
"workday": "Saturday",
|
||||
"start_time": "10:00:00",
|
||||
"end_time": "18:00:00",
|
||||
},
|
||||
{
|
||||
"workday": "Sunday",
|
||||
"start_time": "10:00:00",
|
||||
"end_time": "18:00:00",
|
||||
}
|
||||
]
|
||||
})
|
||||
|
@ -9,7 +9,7 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block page_content %}
|
||||
<div class="item-group-content" itemscope itemtype="http://schema.org/Product">
|
||||
<div class="item-group-content" itemscope itemtype="http://schema.org/Product" data-item-group="{{ name }}">
|
||||
<div class="item-group-slideshow">
|
||||
{% if slideshow %}<!-- slideshow -->
|
||||
{{ web_block(
|
||||
@ -127,15 +127,36 @@
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="row mt-6">
|
||||
<div class="col-3">
|
||||
</div>
|
||||
<div class="col-9">
|
||||
{% if frappe.form_dict.start|int > 0 %}
|
||||
<button class="btn btn-outline-secondary btn-prev" data-start="{{ frappe.form_dict.start|int - page_length }}">{{ _("Prev") }}</button>
|
||||
<button class="btn btn-outline-secondary btn-prev" data-start="{{ frappe.form_dict.start|int - page_length }}">
|
||||
{{ _("Prev") }}
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if items|length >= page_length %}
|
||||
<button class="btn btn-outline-secondary btn-next" data-start="{{ frappe.form_dict.start|int + page_length }}">{{ _("Next") }}</button>
|
||||
<button class="btn btn-outline-secondary btn-next" data-start="{{ frappe.form_dict.start|int + page_length }}"
|
||||
style="float: right;">
|
||||
{{ _("Next") }}
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
frappe.ready(() => {
|
||||
$('.btn-prev, .btn-next').click((e) => {
|
||||
const $btn = $(e.target);
|
||||
$btn.prop('disabled', true);
|
||||
const start = $btn.data('start');
|
||||
let query_params = frappe.utils.get_query_params();
|
||||
query_params.start = start;
|
||||
let path = window.location.pathname + '?' + frappe.utils.get_url_from_dict(query_params);
|
||||
window.location.href = path;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
@ -12,7 +12,7 @@
|
||||
.get_value("User", timesheet.modified_by, [
|
||||
"full_name", "user_image"
|
||||
], as_dict = True)
|
||||
%}
|
||||
%}
|
||||
{% if user_details.user_image %}
|
||||
<span class="avatar avatar-small" style="width:32px; height:32px;" title="{{ user_details.full_name }}">
|
||||
<img src="{{ user_details.user_image }}">
|
||||
|
@ -124,6 +124,10 @@ $(() => {
|
||||
attribute_filters: if_key_exists(attribute_filters)
|
||||
};
|
||||
|
||||
const item_group = $(".item-group-content").data('item-group');
|
||||
if (item_group) {
|
||||
Object.assign(field_filters, { item_group });
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
frappe.call('erpnext.portal.product_configurator.utils.get_products_html_for_website', args)
|
||||
.then(r => {
|
||||
|
Loading…
x
Reference in New Issue
Block a user