Merge branch 'develop' into fix_dn_test

This commit is contained in:
Ankush 2021-08-11 11:40:15 +05:30 committed by GitHub
commit c1c9c4b68e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
215 changed files with 3078 additions and 4619 deletions

View File

@ -1,16 +1,26 @@
name: Backport
on:
pull_request:
pull_request_target:
types:
- closed
- labeled
jobs:
backport:
runs-on: ubuntu-18.04
name: Backport
main:
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- name: Backport
uses: tibdex/backport@v1
- name: Checkout Actions
uses: actions/checkout@v2
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
repository: "frappe/backport"
path: ./actions
ref: develop
- name: Install Actions
run: npm install --production --prefix ./actions
- name: Run backport
uses: ./actions/backport
with:
token: ${{secrets.BACKPORT_BOT_TOKEN}}
labelsToAdd: "backport"
title: "{{originalTitle}}"

View File

@ -6,6 +6,7 @@ on:
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: 'Setup Environment'

View File

@ -5,6 +5,7 @@ on: [pull_request, workflow_dispatch]
jobs:
test:
runs-on: ubuntu-18.04
timeout-minutes: 60
name: Patch Test

View File

@ -9,6 +9,7 @@ on:
jobs:
test:
runs-on: ubuntu-18.04
timeout-minutes: 60
strategy:
fail-fast: false

View File

@ -7,6 +7,7 @@ on:
jobs:
test:
runs-on: ubuntu-18.04
timeout-minutes: 60
strategy:
fail-fast: false

View File

@ -21,13 +21,13 @@ 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/crm/ @ruchamahabal @pateljannat
erpnext/education/ @ruchamahabal @pateljannat
erpnext/healthcare/ @ruchamahabal @pateljannat @chillaranand
erpnext/hr/ @ruchamahabal @pateljannat
erpnext/non_profit/ @ruchamahabal
erpnext/payroll @ruchamahabal
erpnext/projects/ @ruchamahabal
erpnext/payroll @ruchamahabal @pateljannat
erpnext/projects/ @ruchamahabal @pateljannat
erpnext/controllers @deepeshgarg007 @nextchamp-saqib @rohitwaghchaure @marination

View File

@ -5,7 +5,7 @@ import frappe
from erpnext.hooks import regional_overrides
from frappe.utils import getdate
__version__ = '13.7.1'
__version__ = '13.8.0'
def get_default_company(user=None):
'''Get default company for user'''

View File

@ -230,7 +230,7 @@ class Account(NestedSet):
if self.check_gle_exists():
throw(_("Account with existing transaction can not be converted to group."))
elif self.account_type and not self.flags.exclude_account_type_check:
throw(_("Cannot covert to Group because Account Type is selected."))
throw(_("Cannot convert to Group because Account Type is selected."))
else:
self.is_group = 1
self.save()

View File

@ -10,6 +10,7 @@
"accounts_transactions_settings_section",
"over_billing_allowance",
"role_allowed_to_over_bill",
"credit_controller",
"make_payment_via_journal_entry",
"column_break_11",
"check_supplier_invoice_uniqueness",
@ -27,7 +28,6 @@
"acc_frozen_upto",
"frozen_accounts_modifier",
"column_break_4",
"credit_controller",
"deferred_accounting_settings_section",
"book_deferred_entries_based_on",
"column_break_18",
@ -73,11 +73,10 @@
"fieldtype": "Column Break"
},
{
"description": "This role is allowed to submit transactions that exceed credit limits",
"fieldname": "credit_controller",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Credit Controller",
"label": "Role allowed to bypass Credit Limit",
"options": "Role"
},
{
@ -268,7 +267,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2021-06-17 20:26:03.721202",
"modified": "2021-08-09 13:08:01.335416",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",

View File

@ -249,7 +249,7 @@ class TestBudget(unittest.TestCase):
def set_total_expense_zero(posting_date, budget_against_field=None, budget_against_CC=None):
if budget_against_field == "project":
budget_against = "_Test Project"
budget_against = frappe.db.get_value("Project", {"project_name": "_Test Project"})
else:
budget_against = budget_against_CC or "_Test Cost Center - _TC"
@ -275,7 +275,7 @@ def set_total_expense_zero(posting_date, budget_against_field=None, budget_again
"_Test Bank - _TC", -existing_expense, "_Test Cost Center - _TC", posting_date=nowdate(), submit=True)
elif budget_against_field == "project":
make_journal_entry("_Test Account Cost for Goods Sold - _TC",
"_Test Bank - _TC", -existing_expense, "_Test Cost Center - _TC", submit=True, project="_Test Project", posting_date=nowdate())
"_Test Bank - _TC", -existing_expense, "_Test Cost Center - _TC", submit=True, project=budget_against, posting_date=nowdate())
def make_budget(**args):
args = frappe._dict(args)

View File

@ -391,5 +391,5 @@ def set_default_accounts(company):
})
company.save()
install_country_fixtures(company.name)
install_country_fixtures(company.name, company.country)
company.create_default_tax_template()

View File

@ -27,6 +27,9 @@ class ExchangeRateRevaluation(Document):
if not (self.company and self.posting_date):
frappe.throw(_("Please select Company and Posting Date to getting entries"))
def on_cancel(self):
self.ignore_linked_doctypes = ('GL Entry')
@frappe.whitelist()
def check_journal_entry_condition(self):
total_debit = frappe.db.get_value("Journal Entry Account", {
@ -99,10 +102,12 @@ class ExchangeRateRevaluation(Document):
sum(debit) - sum(credit) as balance
from `tabGL Entry`
where account in (%s)
group by account, party_type, party
and posting_date <= %s
and is_cancelled = 0
group by account, NULLIF(party_type,''), NULLIF(party,'')
having sum(debit) != sum(credit)
order by account
""" % ', '.join(['%s']*len(accounts)), tuple(accounts), as_dict=1)
""" % (', '.join(['%s']*len(accounts)), '%s'), tuple(accounts + [self.posting_date]), as_dict=1)
return account_details
@ -143,9 +148,9 @@ class ExchangeRateRevaluation(Document):
"party_type": d.get("party_type"),
"party": d.get("party"),
"account_currency": d.get("account_currency"),
"balance": d.get("balance_in_account_currency"),
dr_or_cr: abs(d.get("balance_in_account_currency")),
"exchange_rate":d.get("new_exchange_rate"),
"balance": flt(d.get("balance_in_account_currency"), d.precision("balance_in_account_currency")),
dr_or_cr: flt(abs(d.get("balance_in_account_currency")), d.precision("balance_in_account_currency")),
"exchange_rate": flt(d.get("new_exchange_rate"), d.precision("new_exchange_rate")),
"reference_type": "Exchange Rate Revaluation",
"reference_name": self.name,
})
@ -154,9 +159,9 @@ class ExchangeRateRevaluation(Document):
"party_type": d.get("party_type"),
"party": d.get("party"),
"account_currency": d.get("account_currency"),
"balance": d.get("balance_in_account_currency"),
reverse_dr_or_cr: abs(d.get("balance_in_account_currency")),
"exchange_rate": d.get("current_exchange_rate"),
"balance": flt(d.get("balance_in_account_currency"), d.precision("balance_in_account_currency")),
reverse_dr_or_cr: flt(abs(d.get("balance_in_account_currency")), d.precision("balance_in_account_currency")),
"exchange_rate": flt(d.get("current_exchange_rate"), d.precision("current_exchange_rate")),
"reference_type": "Exchange Rate Revaluation",
"reference_name": self.name
})
@ -185,9 +190,9 @@ def get_account_details(account, company, posting_date, party_type=None, party=N
account_details = {}
company_currency = erpnext.get_company_currency(company)
balance = get_balance_on(account, party_type=party_type, party=party, in_account_currency=False)
balance = get_balance_on(account, date=posting_date, party_type=party_type, party=party, in_account_currency=False)
if balance:
balance_in_account_currency = get_balance_on(account, party_type=party_type, party=party)
balance_in_account_currency = get_balance_on(account, date=posting_date, party_type=party_type, party=party)
current_exchange_rate = balance / balance_in_account_currency if balance_in_account_currency else 0
new_exchange_rate = get_exchange_rate(account_currency, company_currency, posting_date)
new_balance_in_base_currency = balance_in_account_currency * new_exchange_rate

View File

@ -58,8 +58,8 @@ class GLEntry(Document):
if not self.get(k):
frappe.throw(_("{0} is required").format(_(self.meta.get_label(k))))
account_type = frappe.get_cached_value("Account", self.account, "account_type")
if not (self.party_type and self.party):
account_type = frappe.get_cached_value("Account", self.account, "account_type")
if account_type == "Receivable":
frappe.throw(_("{0} {1}: Customer is required against Receivable account {2}")
.format(self.voucher_type, self.voucher_no, self.account))
@ -73,15 +73,19 @@ class GLEntry(Document):
.format(self.voucher_type, self.voucher_no, self.account))
def pl_must_have_cost_center(self):
if frappe.get_cached_value("Account", self.account, "report_type") == "Profit and Loss":
if not self.cost_center and self.voucher_type != 'Period Closing Voucher':
msg = _("{0} {1}: Cost Center is required for 'Profit and Loss' account {2}.").format(
self.voucher_type, self.voucher_no, self.account)
msg += " "
msg += _("Please set the cost center field in {0} or setup a default Cost Center for the Company.").format(
self.voucher_type)
"""Validate that profit and loss type account GL entries have a cost center."""
frappe.throw(msg, title=_("Missing Cost Center"))
if self.cost_center or self.voucher_type == 'Period Closing Voucher':
return
if frappe.get_cached_value("Account", self.account, "report_type") == "Profit and Loss":
msg = _("{0} {1}: Cost Center is required for 'Profit and Loss' account {2}.").format(
self.voucher_type, self.voucher_no, self.account)
msg += " "
msg += _("Please set the cost center field in {0} or setup a default Cost Center for the Company.").format(
self.voucher_type)
frappe.throw(msg, title=_("Missing Cost Center"))
def validate_dimensions_for_pl_and_bs(self):
account_type = frappe.db.get_value("Account", self.account, "report_type")

View File

@ -306,5 +306,5 @@ def reconcile_dr_cr_note(dr_cr_notes, company):
}
]
})
jv.flags.ignore_mandatory = True
jv.submit()

View File

@ -1545,6 +1545,7 @@
"fieldname": "consolidated_invoice",
"fieldtype": "Link",
"label": "Consolidated Sales Invoice",
"no_copy": 1,
"options": "Sales Invoice",
"read_only": 1
}
@ -1552,7 +1553,7 @@
"icon": "fa fa-file-text",
"is_submittable": 1,
"links": [],
"modified": "2021-02-01 15:03:33.800707",
"modified": "2021-07-29 13:37:20.636171",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Invoice",

View File

@ -558,7 +558,8 @@
"description": "Simple Python Expression, Example: territory != 'All Territories'",
"fieldname": "condition",
"fieldtype": "Code",
"label": "Condition"
"label": "Condition",
"options": "PythonExpression"
},
{
"fieldname": "column_break_42",
@ -575,7 +576,7 @@
"icon": "fa fa-gift",
"idx": 1,
"links": [],
"modified": "2021-03-06 22:01:24.840422",
"modified": "2021-08-06 15:10:04.219321",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Pricing Rule",

View File

@ -15,6 +15,7 @@ from erpnext.healthcare.doctype.lab_test_template.lab_test_template import make_
class TestPricingRule(unittest.TestCase):
def setUp(self):
delete_existing_pricing_rules()
setup_pricing_rule_data()
def tearDown(self):
delete_existing_pricing_rules()
@ -554,6 +555,8 @@ class TestPricingRule(unittest.TestCase):
for doc in [si, si1]:
doc.delete()
test_dependencies = ["Campaign"]
def make_pricing_rule(**args):
args = frappe._dict(args)
@ -600,6 +603,13 @@ def make_pricing_rule(**args):
if args.get(applicable_for):
doc.db_set(applicable_for, args.get(applicable_for))
def setup_pricing_rule_data():
if not frappe.db.exists('Campaign', '_Test Campaign'):
frappe.get_doc({
'doctype': 'Campaign',
'campaign_name': '_Test Campaign',
'name': '_Test Campaign'
}).insert()
def delete_existing_pricing_rules():
for doctype in ["Pricing Rule", "Pricing Rule Item Code",

View File

@ -168,7 +168,7 @@ def _get_tree_conditions(args, parenttype, table, allow_blank=True):
frappe.throw(_("Invalid {0}").format(args.get(field)))
parent_groups = frappe.db.sql_list("""select name from `tab%s`
where lft>=%s and rgt<=%s""" % (parenttype, '%s', '%s'), (lft, rgt))
where lft<=%s and rgt>=%s""" % (parenttype, '%s', '%s'), (lft, rgt))
if parenttype in ["Customer Group", "Item Group", "Territory"]:
parent_field = "parent_{0}".format(frappe.scrub(parenttype))

View File

@ -134,7 +134,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
},
get_query_filters: {
docstatus: 1,
status: ["not in", ["Closed", "Completed"]],
status: ["not in", ["Closed", "Completed", "Return Issued"]],
company: me.frm.doc.company,
is_return: 0
}

View File

@ -22,11 +22,13 @@ from erpnext.assets.doctype.asset.asset import get_asset_account, is_cwip_accoun
from frappe.model.mapper import get_mapped_doc
from six import iteritems
from erpnext.accounts.doctype.sales_invoice.sales_invoice import validate_inter_company_party, update_linked_doc,\
unlink_inter_company_doc
unlink_inter_company_doc, check_if_return_invoice_linked_with_payment_entry
from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import get_party_tax_withholding_details
from erpnext.accounts.deferred_revenue import validate_service_stop_date
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import get_item_account_wise_additional_cost
class WarehouseMissingError(frappe.ValidationError): pass
form_grid_templates = {
"items": "templates/form_grid/item_grid.html"
}
@ -207,8 +209,8 @@ class PurchaseInvoice(BuyingController):
if self.update_stock and for_validate:
for d in self.get('items'):
if not d.warehouse:
frappe.throw(_("Warehouse required at Row No {0}, please set default warehouse for the item {1} for the company {2}").
format(d.idx, d.item_code, self.company))
frappe.throw(_("Row No {0}: Warehouse is required. Please set a Default Warehouse for Item {1} and Company {2}").
format(d.idx, d.item_code, self.company), exc=WarehouseMissingError)
super(PurchaseInvoice, self).validate_warehouse()
@ -246,7 +248,7 @@ class PurchaseInvoice(BuyingController):
and (not item.po_detail or
not frappe.db.get_value("Purchase Order Item", item.po_detail, "delivered_by_supplier")):
if self.update_stock and (not item.from_warehouse):
if self.update_stock and item.warehouse and (not item.from_warehouse):
if for_validate and item.expense_account and item.expense_account != warehouse_account[item.warehouse]["account"]:
msg = _("Row {0}: Expense Head changed to {1} because account {2} is not linked to warehouse {3} or it is not the default inventory account").format(
item.idx, frappe.bold(warehouse_account[item.warehouse]["account"]), frappe.bold(item.expense_account), frappe.bold(item.warehouse))
@ -657,7 +659,7 @@ class PurchaseInvoice(BuyingController):
)
gl_entries.append(
self.get_gl_dict({
"account": self.get_company_default("exchange_gain_loss_account"),
"account": self.get_company_default("exchange_gain_loss_account"),
"against": self.supplier,
"credit": discrepancy_caused_by_exchange_rate_difference,
"cost_center": item.cost_center,
@ -1012,6 +1014,8 @@ class PurchaseInvoice(BuyingController):
}, item=self))
def on_cancel(self):
check_if_return_invoice_linked_with_payment_entry(self)
super(PurchaseInvoice, self).on_cancel()
self.check_on_hold_or_closed_status()
@ -1193,7 +1197,7 @@ def get_purchase_document_details(doc):
purchase_receipts_or_invoices.append(item.get(doc_reference))
if item.get(items_reference):
items.append(item.get(items_reference))
exchange_rate_map = frappe._dict(frappe.get_all(parent_doctype, filters={'name': ('in',
purchase_receipts_or_invoices)}, fields=['name', 'conversion_rate'], as_list=1))

View File

@ -240,7 +240,7 @@ class TestPurchaseInvoice(unittest.TestCase):
pi.conversion_rate = 80
pi.insert()
pi.submit()
pi.submit()
# Get exchnage gain and loss account
exchange_gain_loss_account = frappe.db.get_value('Company', pi.company, 'exchange_gain_loss_account')
@ -978,7 +978,7 @@ class TestPurchaseInvoice(unittest.TestCase):
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)
@ -1018,8 +1018,8 @@ class TestPurchaseInvoice(unittest.TestCase):
expected_gle = [
["_Test Account Cost for Goods Sold - _TC", 37500.0],
["_Test Payable USD - _TC", -40000.0],
["Exchange Gain/Loss - _TC", 2500.0]
["_Test Payable USD - _TC", -35000.0],
["Exchange Gain/Loss - _TC", -2500.0]
]
gl_entries = frappe.db.sql("""
@ -1027,7 +1027,7 @@ class TestPurchaseInvoice(unittest.TestCase):
where voucher_no=%s
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)
self.assertEqual(expected_gle[i][1], gle.balance)
@ -1049,8 +1049,8 @@ class TestPurchaseInvoice(unittest.TestCase):
expected_gle = [
["_Test Account Cost for Goods Sold - _TC", 36500.0],
["_Test Payable USD - _TC", -38000.0],
["Exchange Gain/Loss - _TC", 1500.0]
["_Test Payable USD - _TC", -35000.0],
["Exchange Gain/Loss - _TC", -1500.0]
]
gl_entries = frappe.db.sql("""

View File

@ -290,6 +290,8 @@ class SalesInvoice(SellingController):
self.update_time_sheet(None)
def on_cancel(self):
check_if_return_invoice_linked_with_payment_entry(self)
super(SalesInvoice, self).on_cancel()
self.check_sales_order_on_hold_or_close("sales_order")
@ -480,7 +482,7 @@ class SalesInvoice(SellingController):
if not self.pos_profile:
pos_profile = get_pos_profile(self.company) or {}
if not pos_profile:
frappe.throw(_("No POS Profile found. Please create a New POS Profile first"))
return
self.pos_profile = pos_profile.get('name')
pos = {}
@ -922,7 +924,7 @@ class SalesInvoice(SellingController):
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),
"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
@ -944,7 +946,7 @@ class SalesInvoice(SellingController):
gl_entries.append(self.get_gl_dict(gle, item=item))
self.set_asset_status(asset)
else:
# Do not book income for transfer within same company
if not self.is_internal_transfer():
@ -973,7 +975,7 @@ class SalesInvoice(SellingController):
def set_asset_status(self, asset):
if self.is_return:
asset.set_status()
else:
else:
asset.set_status("Sold" if self.docstatus==1 else None)
def make_loyalty_point_redemption_gle(self, gl_entries):
@ -1941,3 +1943,41 @@ def create_dunning(source_name, target_doc=None):
}
}, target_doc, set_missing_values)
return doclist
def check_if_return_invoice_linked_with_payment_entry(self):
# If a Return invoice is linked with payment entry along with other invoices,
# the cancellation of the Return causes allocated amount to be greater than paid
if not frappe.db.get_single_value('Accounts Settings', 'unlink_payment_on_cancellation_of_invoice'):
return
payment_entries = []
if self.is_return and self.return_against:
invoice = self.return_against
else:
invoice = self.name
payment_entries = frappe.db.sql_list("""
SELECT
t1.name
FROM
`tabPayment Entry` t1, `tabPayment Entry Reference` t2
WHERE
t1.name = t2.parent
and t1.docstatus = 1
and t2.reference_name = %s
and t2.allocated_amount < 0
""", invoice)
links_to_pe = []
if payment_entries:
for payment in payment_entries:
payment_entry = frappe.get_doc("Payment Entry", payment)
if len(payment_entry.references) > 1:
links_to_pe.append(payment_entry.name)
if links_to_pe:
payment_entries_link = [get_link_to_form('Payment Entry', name, label=name) for name in links_to_pe]
message = _("Please cancel and amend the Payment Entry")
message += " " + ", ".join(payment_entries_link) + " "
message += _("to unallocate the amount of this Return Invoice before cancelling it.")
frappe.throw(message)

View File

@ -2,13 +2,14 @@
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
import frappe
import frappe, erpnext
import unittest, copy, time
from frappe.utils import nowdate, flt, getdate, cint, add_days, add_months
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.purchase_invoice.purchase_invoice import WarehouseMissingError
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
@ -1073,7 +1074,7 @@ class TestSalesInvoice(unittest.TestCase):
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)
@ -1081,7 +1082,7 @@ class TestSalesInvoice(unittest.TestCase):
# 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",
"GL Entry",
filters = {
"voucher_no": si.name,
"account": disposal_account
@ -1090,7 +1091,7 @@ class TestSalesInvoice(unittest.TestCase):
)[0]
loss_for_return_si = frappe.get_all(
"GL Entry",
"GL Entry",
filters = {
"voucher_no": return_si.name,
"account": disposal_account
@ -1836,6 +1837,89 @@ class TestSalesInvoice(unittest.TestCase):
self.assertEqual(target_doc.company, "_Test Company 1")
self.assertEqual(target_doc.supplier, "_Test Internal Supplier")
def test_inter_company_transaction_without_default_warehouse(self):
"Check mapping (expense account) of inter company SI to PI in absence of default warehouse."
# setup
old_negative_stock = frappe.db.get_single_value("Stock Settings", "allow_negative_stock")
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1)
old_perpetual_inventory = erpnext.is_perpetual_inventory_enabled('_Test Company 1')
frappe.local.enable_perpetual_inventory['_Test Company 1'] = 1
frappe.db.set_value("Company", '_Test Company 1', "stock_received_but_not_billed", "Stock Received But Not Billed - _TC1")
frappe.db.set_value("Company", '_Test Company 1', "expenses_included_in_valuation", "Expenses Included In Valuation - _TC1")
if not frappe.db.exists("Customer", "_Test Internal Customer"):
customer = frappe.get_doc({
"customer_group": "_Test Customer Group",
"customer_name": "_Test Internal Customer",
"customer_type": "Individual",
"doctype": "Customer",
"territory": "_Test Territory",
"is_internal_customer": 1,
"represents_company": "_Test Company 1"
})
customer.append("companies", {
"company": "Wind Power LLC"
})
customer.insert()
if not frappe.db.exists("Supplier", "_Test Internal Supplier"):
supplier = frappe.get_doc({
"supplier_group": "_Test Supplier Group",
"supplier_name": "_Test Internal Supplier",
"doctype": "Supplier",
"is_internal_supplier": 1,
"represents_company": "Wind Power LLC"
})
supplier.append("companies", {
"company": "_Test Company 1"
})
supplier.insert()
# begin test
si = create_sales_invoice(
company = "Wind Power LLC",
customer = "_Test Internal Customer",
debit_to = "Debtors - WP",
warehouse = "Stores - WP",
income_account = "Sales - WP",
expense_account = "Cost of Goods Sold - WP",
cost_center = "Main - WP",
currency = "USD",
update_stock = 1,
do_not_save = 1
)
si.selling_price_list = "_Test Price List Rest of the World"
si.submit()
target_doc = make_inter_company_transaction("Sales Invoice", si.name)
# in absence of warehouse Stock Received But Not Billed is set as expense account while mapping
# mapping is not obstructed
self.assertIsNone(target_doc.items[0].warehouse)
self.assertEqual(target_doc.items[0].expense_account, "Stock Received But Not Billed - _TC1")
target_doc.items[0].update({"cost_center": "Main - _TC1"})
# missing warehouse is validated on save, after mapping
self.assertRaises(WarehouseMissingError, target_doc.save)
target_doc.items[0].update({"warehouse": "Stores - _TC1"})
target_doc.save()
# after warehouse is set, linked account or default inventory account is set
self.assertEqual(target_doc.items[0].expense_account, 'Stock In Hand - _TC1')
# tear down
frappe.local.enable_perpetual_inventory['_Test Company 1'] = old_perpetual_inventory
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", old_negative_stock)
def test_internal_transfer_gl_entry(self):
## Create internal transfer account
account = create_account(account_name="Unrealized Profit",

View File

@ -27,7 +27,8 @@
"base_tax_amount",
"base_total",
"base_tax_amount_after_discount_amount",
"item_wise_tax_detail"
"item_wise_tax_detail",
"dont_recompute_tax"
],
"fields": [
{
@ -200,13 +201,22 @@
"fieldname": "included_in_paid_amount",
"fieldtype": "Check",
"label": "Considered In Paid Amount"
},
{
"default": "0",
"fieldname": "dont_recompute_tax",
"fieldtype": "Check",
"hidden": 1,
"label": "Dont Recompute tax",
"print_hide": 1,
"read_only": 1
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-06-14 01:44:36.899147",
"modified": "2021-07-27 12:40:59.051803",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Taxes and Charges",

View File

@ -6,7 +6,7 @@ import frappe
from frappe import _
from frappe.utils import flt
from frappe.model.document import Document
from erpnext.controllers.accounts_controller import validate_taxes_and_charges, validate_inclusive_tax
from erpnext.controllers.accounts_controller import validate_taxes_and_charges, validate_inclusive_tax, validate_cost_center, validate_account_head
class SalesTaxesandChargesTemplate(Document):
def validate(self):
@ -39,6 +39,8 @@ def valdiate_taxes_and_charges_template(doc):
for tax in doc.get("taxes"):
validate_taxes_and_charges(tax)
validate_account_head(tax, doc)
validate_cost_center(tax, doc)
validate_inclusive_tax(tax, doc)
def validate_disabled(doc):

View File

@ -8,6 +8,7 @@
"charge_type": "On Net Total",
"description": "VAT",
"doctype": "Sales Taxes and Charges",
"cost_center": "Main - _TC",
"parentfield": "taxes",
"rate": 6
},
@ -16,6 +17,7 @@
"charge_type": "On Net Total",
"description": "Service Tax",
"doctype": "Sales Taxes and Charges",
"cost_center": "Main - _TC",
"parentfield": "taxes",
"rate": 6.36
}
@ -114,6 +116,7 @@
"charge_type": "On Net Total",
"description": "VAT",
"doctype": "Sales Taxes and Charges",
"cost_center": "Main - _TC",
"parentfield": "taxes",
"rate": 12
},
@ -122,6 +125,7 @@
"charge_type": "On Net Total",
"description": "Service Tax",
"doctype": "Sales Taxes and Charges",
"cost_center": "Main - _TC",
"parentfield": "taxes",
"rate": 4
}
@ -137,6 +141,7 @@
"charge_type": "On Net Total",
"description": "VAT",
"doctype": "Sales Taxes and Charges",
"cost_center": "Main - _TC",
"parentfield": "taxes",
"rate": 12
},
@ -145,6 +150,7 @@
"charge_type": "On Net Total",
"description": "Service Tax",
"doctype": "Sales Taxes and Charges",
"cost_center": "Main - _TC",
"parentfield": "taxes",
"rate": 4
}
@ -160,6 +166,7 @@
"charge_type": "On Net Total",
"description": "VAT",
"doctype": "Sales Taxes and Charges",
"cost_center": "Main - _TC",
"parentfield": "taxes",
"rate": 12
},
@ -168,6 +175,7 @@
"charge_type": "On Net Total",
"description": "Service Tax",
"doctype": "Sales Taxes and Charges",
"cost_center": "Main - _TC",
"parentfield": "taxes",
"rate": 4
}

View File

@ -0,0 +1,34 @@
{
"actions": [],
"autoname": "account",
"creation": "2021-07-08 22:04:24.634967",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"account"
],
"fields": [
{
"allow_in_quick_entry": 1,
"fieldname": "account",
"fieldtype": "Link",
"in_list_view": 1,
"in_preview": 1,
"label": "Account",
"options": "Account"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-07-08 22:35:33.202911",
"modified_by": "Administrator",
"module": "Accounts",
"name": "South Africa VAT Account",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

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

View File

@ -78,7 +78,7 @@
"label": "Cost"
},
{
"depends_on": "eval:doc.price_determination==\"Based on price list\"",
"depends_on": "eval:doc.price_determination==\"Based On Price List\"",
"fieldname": "price_list",
"fieldtype": "Link",
"label": "Price List",
@ -147,7 +147,7 @@
}
],
"links": [],
"modified": "2020-06-25 10:53:44.205774",
"modified": "2021-08-09 10:53:44.205774",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Subscription Plan",

View File

@ -1,263 +1,151 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 1,
"allow_rename": 1,
"autoname": "Prompt",
"beta": 0,
"creation": "2018-04-13 18:42:06.431683",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"actions": [],
"allow_import": 1,
"allow_rename": 1,
"autoname": "Prompt",
"creation": "2018-04-13 18:42:06.431683",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"category_details_section",
"category_name",
"round_off_tax_amount",
"column_break_2",
"consider_party_ledger_amount",
"tax_on_excess_amount",
"section_break_8",
"rates",
"section_break_7",
"accounts"
],
"fields": [
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "category_name",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Category Name",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"show_days": 1,
"show_seconds": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "section_break_8",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Tax Withholding Rates",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"show_days": 1,
"show_seconds": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "rates",
"fieldtype": "Table",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Rates",
"length": 0,
"no_copy": 0,
"options": "Tax Withholding Rate",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"show_days": 1,
"show_seconds": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "section_break_7",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"fieldname": "section_break_7",
"fieldtype": "Section Break",
"label": "Account Details",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"show_days": 1,
"show_seconds": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "accounts",
"fieldtype": "Table",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Accounts",
"length": 0,
"no_copy": 0,
"options": "Tax Withholding Account",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"fieldname": "accounts",
"fieldtype": "Table",
"label": "Accounts",
"options": "Tax Withholding Account",
"reqd": 1,
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "category_details_section",
"fieldtype": "Section Break",
"label": "Category Details",
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "column_break_2",
"fieldtype": "Column Break",
"show_days": 1,
"show_seconds": 1
},
{
"default": "0",
"description": "Even invoices with apply tax withholding unchecked will be considered for checking cumulative threshold breach",
"fieldname": "consider_party_ledger_amount",
"fieldtype": "Check",
"label": "Consider Entire Party Ledger Amount",
"show_days": 1,
"show_seconds": 1
},
{
"default": "0",
"description": "Tax will be withheld only for amount exceeding the cumulative threshold",
"fieldname": "tax_on_excess_amount",
"fieldtype": "Check",
"label": "Only Deduct Tax On Excess Amount ",
"show_days": 1,
"show_seconds": 1
},
{
"description": "Checking this will round off the tax amount to the nearest integer",
"fieldname": "round_off_tax_amount",
"fieldtype": "Check",
"label": "Round Off Tax Amount",
"show_days": 1,
"show_seconds": 1
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2018-07-17 22:53:26.193179",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Tax Withholding Category",
"name_case": "",
"owner": "Administrator",
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-07-27 21:47:34.396071",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Tax Withholding Category",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
},
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts Manager",
"share": 1,
"write": 1
},
},
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts User",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts User",
"share": 1,
"write": 1
}
],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0,
"track_views": 0
],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -6,7 +6,7 @@ from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import flt, getdate
from frappe.utils import flt, getdate, cint
from erpnext.accounts.utils import get_fiscal_year
class TaxWithholdingCategory(Document):
@ -86,7 +86,10 @@ def get_tax_withholding_details(tax_withholding_category, fiscal_year, company):
"rate": tax_rate_detail.tax_withholding_rate,
"threshold": tax_rate_detail.single_threshold,
"cumulative_threshold": tax_rate_detail.cumulative_threshold,
"description": tax_withholding.category_name if tax_withholding.category_name else tax_withholding_category
"description": tax_withholding.category_name if tax_withholding.category_name else tax_withholding_category,
"consider_party_ledger_amount": tax_withholding.consider_party_ledger_amount,
"tax_on_excess_amount": tax_withholding.tax_on_excess_amount,
"round_off_tax_amount": tax_withholding.round_off_tax_amount
})
def get_tax_withholding_rates(tax_withholding, fiscal_year):
@ -145,6 +148,7 @@ def get_lower_deduction_certificate(fiscal_year, pan_no):
def get_tax_amount(party_type, parties, inv, tax_details, fiscal_year_details, pan_no=None):
fiscal_year = fiscal_year_details[0]
vouchers = get_invoice_vouchers(parties, fiscal_year, inv.company, party_type=party_type)
advance_vouchers = get_advance_vouchers(parties, fiscal_year, inv.company, party_type=party_type)
taxable_vouchers = vouchers + advance_vouchers
@ -235,10 +239,18 @@ def get_deducted_tax(taxable_vouchers, fiscal_year, tax_details):
def get_tds_amount(ldc, parties, inv, tax_details, fiscal_year_details, tax_deducted, vouchers):
tds_amount = 0
invoice_filters = {
'name': ('in', vouchers),
'docstatus': 1
}
supp_credit_amt = frappe.db.get_value('Purchase Invoice', {
'name': ('in', vouchers), 'docstatus': 1, 'apply_tds': 1
}, 'sum(net_total)') or 0.0
field = 'sum(net_total)'
if not cint(tax_details.consider_party_ledger_amount):
invoice_filters.update({'apply_tds': 1})
field = 'sum(grand_total)'
supp_credit_amt = frappe.db.get_value('Purchase Invoice', invoice_filters, field) or 0.0
supp_jv_credit_amt = frappe.db.get_value('Journal Entry Account', {
'parent': ('in', vouchers), 'docstatus': 1,
@ -255,6 +267,13 @@ def get_tds_amount(ldc, parties, inv, tax_details, fiscal_year_details, tax_dedu
cumulative_threshold = tax_details.get('cumulative_threshold', 0)
if ((threshold and inv.net_total >= threshold) or (cumulative_threshold and supp_credit_amt >= cumulative_threshold)):
if (cumulative_threshold and supp_credit_amt >= cumulative_threshold) and cint(tax_details.tax_on_excess_amount):
# Get net total again as TDS is calculated on net total
# Grand is used to just check for threshold breach
net_total = frappe.db.get_value('Purchase Invoice', invoice_filters, 'sum(net_total)') or 0.0
net_total += inv.net_total
supp_credit_amt = net_total - cumulative_threshold
if ldc and is_valid_certificate(
ldc.valid_from, ldc.valid_upto,
inv.get('posting_date') or inv.get('transaction_date'), tax_deducted,
@ -263,6 +282,9 @@ def get_tds_amount(ldc, parties, inv, tax_details, fiscal_year_details, tax_dedu
tds_amount = get_ltds_amount(supp_credit_amt, 0, ldc.certificate_limit, ldc.rate, tax_details)
else:
tds_amount = supp_credit_amt * tax_details.rate / 100 if supp_credit_amt > 0 else 0
if cint(tax_details.round_off_tax_amount):
tds_amount = round(tds_amount)
return tds_amount

View File

@ -87,6 +87,31 @@ class TestTaxWithholdingCategory(unittest.TestCase):
for d in invoices:
d.cancel()
def test_tax_withholding_category_checks(self):
invoices = []
frappe.db.set_value("Supplier", "Test TDS Supplier3", "tax_withholding_category", "New TDS Category")
# First Invoice with no tds check
pi = create_purchase_invoice(supplier = "Test TDS Supplier3", rate = 20000, do_not_save=True)
pi.apply_tds = 0
pi.save()
pi.submit()
invoices.append(pi)
# Second Invoice will apply TDS checked
pi1 = create_purchase_invoice(supplier = "Test TDS Supplier3", rate = 20000)
pi1.submit()
invoices.append(pi1)
# Cumulative threshold is 30000
# Threshold calculation should be on both the invoices
# TDS should be applied only on 1000
self.assertEqual(pi1.taxes[0].tax_amount, 1000)
for d in invoices:
d.cancel()
def test_cumulative_threshold_tcs(self):
frappe.db.set_value("Customer", "Test TCS Customer", "tax_withholding_category", "Cumulative Threshold TCS")
invoices = []
@ -195,7 +220,7 @@ def create_sales_invoice(**args):
def create_records():
# create a new suppliers
for name in ['Test TDS Supplier', 'Test TDS Supplier1', 'Test TDS Supplier2']:
for name in ['Test TDS Supplier', 'Test TDS Supplier1', 'Test TDS Supplier2', 'Test TDS Supplier3']:
if frappe.db.exists('Supplier', name):
continue
@ -311,3 +336,23 @@ def create_tax_with_holding_category():
'account': 'TDS - _TC'
}]
}).insert()
if not frappe.db.exists("Tax Withholding Category", "New TDS Category"):
frappe.get_doc({
"doctype": "Tax Withholding Category",
"name": "New TDS Category",
"category_name": "New TDS Category",
"round_off_tax_amount": 1,
"consider_party_ledger_amount": 1,
"tax_on_excess_amount": 1,
"rates": [{
'fiscal_year': fiscal_year,
'tax_withholding_rate': 10,
'single_threshold': 0,
'cumulative_threshold': 30000
}],
"accounts": [{
'company': '_Test Company',
'account': 'TDS - _TC'
}]
}).insert()

View File

@ -100,8 +100,8 @@ def merge_similar_entries(gl_map, precision=None):
return merged_gl_map
def check_if_in_list(gle, gl_map, dimensions=None):
account_head_fieldnames = ['party_type', 'party', 'against_voucher', 'against_voucher_type',
'cost_center', 'project', 'voucher_detail_no']
account_head_fieldnames = ['voucher_detail_no', 'party', 'against_voucher',
'cost_center', 'against_voucher_type', 'party_type', 'project']
if dimensions:
account_head_fieldnames = account_head_fieldnames + dimensions
@ -110,10 +110,12 @@ def check_if_in_list(gle, gl_map, dimensions=None):
same_head = True
if e.account != gle.account:
same_head = False
continue
for fieldname in account_head_fieldnames:
if cstr(e.get(fieldname)) != cstr(gle.get(fieldname)):
same_head = False
break
if same_head:
return e
@ -143,16 +145,19 @@ def make_entry(args, adv_adj, update_outstanding, from_repost=False):
validate_expense_against_budget(args)
def validate_cwip_accounts(gl_map):
cwip_enabled = any(cint(ac.enable_cwip_accounting) for ac in frappe.db.get_all("Asset Category","enable_cwip_accounting"))
"""Validate that CWIP account are not used in Journal Entry"""
if gl_map and gl_map[0].voucher_type != "Journal Entry":
return
if cwip_enabled and gl_map[0].voucher_type == "Journal Entry":
cwip_accounts = [d[0] for d in frappe.db.sql("""select name from tabAccount
where account_type = 'Capital Work in Progress' and is_group=0""")]
cwip_enabled = any(cint(ac.enable_cwip_accounting) for ac in frappe.db.get_all("Asset Category", "enable_cwip_accounting"))
if cwip_enabled:
cwip_accounts = [d[0] for d in frappe.db.sql("""select name from tabAccount
where account_type = 'Capital Work in Progress' and is_group=0""")]
for entry in gl_map:
if entry.account in cwip_accounts:
frappe.throw(
_("Account: <b>{0}</b> is capital Work in progress and can not be updated by Journal Entry").format(entry.account))
for entry in gl_map:
if entry.account in cwip_accounts:
frappe.throw(
_("Account: <b>{0}</b> is capital Work in progress and can not be updated by Journal Entry").format(entry.account))
def round_off_debit_credit(gl_map):
precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"),

View File

@ -99,7 +99,6 @@ class ReceivablePayableReport(object):
voucher_no = gle.voucher_no,
party = gle.party,
posting_date = gle.posting_date,
remarks = gle.remarks,
account_currency = gle.account_currency,
invoiced = 0.0,
paid = 0.0,
@ -579,7 +578,7 @@ class ReceivablePayableReport(object):
self.gl_entries = frappe.db.sql("""
select
name, posting_date, account, party_type, party, voucher_type, voucher_no, cost_center,
against_voucher_type, against_voucher, account_currency, remarks, {0}
against_voucher_type, against_voucher, account_currency, {0}
from
`tabGL Entry`
where
@ -792,8 +791,6 @@ class ReceivablePayableReport(object):
self.add_column(label=_('Supplier Group'), fieldname='supplier_group', fieldtype='Link',
options='Supplier Group')
self.add_column(label=_('Remarks'), fieldname='remarks', fieldtype='Text', width=200)
def add_column(self, label, fieldname=None, fieldtype='Currency', options=None, width=120):
if not fieldname: fieldname = scrub(label)
if fieldtype=='Currency': options='currency'

View File

@ -241,6 +241,7 @@ class GrossProfitGenerator(object):
sle.voucher_detail_no == row.item_row:
previous_stock_value = len(my_sle) > i+1 and \
flt(my_sle[i+1].stock_value) or 0.0
if previous_stock_value:
return (previous_stock_value - flt(sle.stock_value)) * flt(row.qty) / abs(flt(sle.qty))
else:
@ -335,7 +336,7 @@ class GrossProfitGenerator(object):
res = frappe.db.sql("""select item_code, voucher_type, voucher_no,
voucher_detail_no, stock_value, warehouse, actual_qty as qty
from `tabStock Ledger Entry`
where company=%(company)s
where company=%(company)s and is_cancelled = 0
order by
item_code desc, warehouse desc, posting_date desc,
posting_time desc, creation desc""", self.filters, as_dict=True)

View File

@ -566,10 +566,10 @@ def remove_ref_doc_link_from_pe(ref_type, ref_no):
frappe.msgprint(_("Payment Entries {0} are un-linked").format("\n".join(linked_pe)))
@frappe.whitelist()
def get_company_default(company, fieldname):
value = frappe.get_cached_value('Company', company, fieldname)
def get_company_default(company, fieldname, ignore_validation=False):
value = frappe.get_cached_value('Company', company, fieldname)
if not value:
if not ignore_validation and not value:
throw(_("Please set default {0} in Company {1}")
.format(frappe.get_meta("Company").get_label(fieldname), company))
@ -920,7 +920,6 @@ def repost_gle_for_stock_vouchers(stock_vouchers, posting_date, company=None, wa
_delete_gl_entries(voucher_type, voucher_no)
def get_future_stock_vouchers(posting_date, posting_time, for_warehouses=None, for_items=None, company=None):
future_stock_vouchers = []
values = []
condition = ""
@ -936,37 +935,53 @@ def get_future_stock_vouchers(posting_date, posting_time, for_warehouses=None, f
condition += " and company = %s"
values.append(company)
for d in frappe.db.sql("""select distinct sle.voucher_type, sle.voucher_no
future_stock_vouchers = frappe.db.sql("""select distinct sle.voucher_type, sle.voucher_no
from `tabStock Ledger Entry` sle
where
timestamp(sle.posting_date, sle.posting_time) >= timestamp(%s, %s)
and is_cancelled = 0
{condition}
order by timestamp(sle.posting_date, sle.posting_time) asc, creation asc for update""".format(condition=condition),
tuple([posting_date, posting_time] + values), as_dict=True):
future_stock_vouchers.append([d.voucher_type, d.voucher_no])
tuple([posting_date, posting_time] + values), as_dict=True)
return future_stock_vouchers
return [(d.voucher_type, d.voucher_no) for d in future_stock_vouchers]
def get_voucherwise_gl_entries(future_stock_vouchers, posting_date):
""" Get voucherwise list of GL entries.
Only fetches GLE fields required for comparing with new GLE.
Check compare_existing_and_expected_gle function below.
"""
gl_entries = {}
if future_stock_vouchers:
for d in frappe.db.sql("""select * from `tabGL Entry`
where posting_date >= %s and voucher_no in (%s)""" %
('%s', ', '.join(['%s']*len(future_stock_vouchers))),
tuple([posting_date] + [d[1] for d in future_stock_vouchers]), as_dict=1):
gl_entries.setdefault((d.voucher_type, d.voucher_no), []).append(d)
if not future_stock_vouchers:
return gl_entries
voucher_nos = [d[1] for d in future_stock_vouchers]
gles = frappe.db.sql("""
select name, account, credit, debit, cost_center, project
from `tabGL Entry`
where
posting_date >= %s and voucher_no in (%s)""" %
('%s', ', '.join(['%s'] * len(voucher_nos))),
tuple([posting_date] + voucher_nos), as_dict=1)
for d in gles:
gl_entries.setdefault((d.voucher_type, d.voucher_no), []).append(d)
return gl_entries
def compare_existing_and_expected_gle(existing_gle, expected_gle, precision):
if len(existing_gle) != len(expected_gle):
return False
matched = True
for entry in expected_gle:
account_existed = False
for e in existing_gle:
if entry.account == e.account:
account_existed = True
if (entry.account == e.account and entry.against_account == e.against_account
if (entry.account == e.account
and (not entry.cost_center or not e.cost_center or entry.cost_center == e.cost_center)
and ( flt(entry.debit, precision) != flt(e.debit, precision) or
flt(entry.credit, precision) != flt(e.credit, precision))):

View File

@ -639,7 +639,7 @@ class TestAsset(unittest.TestCase):
asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name')
asset = frappe.get_doc('Asset', asset_name)
asset.calculate_depreciation = 1
asset.available_for_use_date = '2030-06-12'
asset.available_for_use_date = '2030-07-12'
asset.purchase_date = '2030-01-01'
asset.append("finance_books", {
"expected_value_after_useful_life": 1000,
@ -653,10 +653,10 @@ class TestAsset(unittest.TestCase):
self.assertEqual(asset.finance_books[0].rate_of_depreciation, 50.0)
expected_schedules = [
["2030-12-31", 1106.85, 1106.85],
["2031-12-31", 3446.58, 4553.43],
["2032-12-31", 1723.29, 6276.72],
["2033-06-12", 723.28, 7000.00]
["2030-12-31", 942.47, 942.47],
["2031-12-31", 3528.77, 4471.24],
["2032-12-31", 1764.38, 6235.62],
["2033-07-12", 764.38, 7000.00]
]
schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), flt(d.accumulated_depreciation_amount, 2)]

View File

@ -0,0 +1,39 @@
# Version 13.8.0 Release Notes
### Features & Enhancements
- Report to show COGS by item groups ([#26222](https://github.com/frappe/erpnext/pull/26222))
- Enhancements in TDS ([#26677](https://github.com/frappe/erpnext/pull/26677))
- API Endpoint to update halted Razorpay subscriptions ([#26564](https://github.com/frappe/erpnext/pull/26564))
### Fixes
- Incorrect bom name ([#26600](https://github.com/frappe/erpnext/pull/26600))
- Exchange rate revaluation posting date and precision fixes ([#26651](https://github.com/frappe/erpnext/pull/26651))
- POS item cart dom updates ([#26460](https://github.com/frappe/erpnext/pull/26460))
- General Ledger report not working with filter group by ([#26439](https://github.com/frappe/erpnext/pull/26438))
- Tax calculation for Recurring additional salary ([#24206](https://github.com/frappe/erpnext/pull/24206))
- Validation check for batch for stock reconciliation type in stock entry ([#26487](https://github.com/frappe/erpnext/pull/26487))
- Improved UX for additional discount field ([#26502](https://github.com/frappe/erpnext/pull/26502))
- Add missing cess amount in GSTR-3B report ([#26644](https://github.com/frappe/erpnext/pull/26644))
- Optimized code for reposting item valuation ([#26431](https://github.com/frappe/erpnext/pull/26431))
- FG item not fetched in manufacture entry ([#26508](https://github.com/frappe/erpnext/pull/26508))
- Errors on parallel requests creation of company for India ([#26420](https://github.com/frappe/erpnext/pull/26420))
- Incorrect valuation rate calculation in gross profit report ([#26558](https://github.com/frappe/erpnext/pull/26558))
- Empty "against account" in Purchase Receipt GLE ([#26712](https://github.com/frappe/erpnext/pull/26712))
- Remove cancelled entries from Stock and Account Value comparison report ([#26721](https://github.com/frappe/erpnext/pull/26721))
- Remove manual permission checking ([#26691](https://github.com/frappe/erpnext/pull/26691))
- Delete child docs when parent doc is deleted ([#26518](https://github.com/frappe/erpnext/pull/26518))
- GST Reports timeout issue ([#26646](https://github.com/frappe/erpnext/pull/26646))
- Parent condition in pricing rules ([#26727](https://github.com/frappe/erpnext/pull/26727))
- Added Company filters for Loan ([#26294](https://github.com/frappe/erpnext/pull/26294))
- Incorrect discount amount on amended document ([#26292](https://github.com/frappe/erpnext/pull/26292))
- Exchange gain loss not set for advances linked with invoices ([#26436](https://github.com/frappe/erpnext/pull/26436))
- Unallocated amount in Payment Entry after taxes ([#26412](https://github.com/frappe/erpnext/pull/26412))
- Wrong operation time in Work Order ([#26613](https://github.com/frappe/erpnext/pull/26613))
- Serial No and Batch validation ([#26614](https://github.com/frappe/erpnext/pull/26614))
- Gl Entries for exchange gain loss ([#26734](https://github.com/frappe/erpnext/pull/26734))
- TDS computation summary shows cancelled invoices ([#26485](https://github.com/frappe/erpnext/pull/26485))
- Price List rate not fetched for return sales invoice fixed ([#26560](https://github.com/frappe/erpnext/pull/26560))
- Included company in link document type filters for contact ([#26576](https://github.com/frappe/erpnext/pull/26576))
- Ignore mandatory fields while creating payment reconciliation Journal Entry ([#26643](https://github.com/frappe/erpnext/pull/26643))
- Unable to download GSTR-1 json ([#26418](https://github.com/frappe/erpnext/pull/26418))
- Paging buttons not working on item group portal page ([#26498](https://github.com/frappe/erpnext/pull/26498))

View File

@ -674,19 +674,24 @@ class AccountsController(TransactionBase):
if self.get('doctype') in ['Purchase Invoice', 'Sales Invoice']:
for d in self.get("advances"):
if d.exchange_gain_loss:
party = self.supplier if self.get('doctype') == 'Purchase Invoice' else self.customer
party_account = self.credit_to if self.get('doctype') == 'Purchase Invoice' else self.debit_to
party_type = "Supplier" if self.get('doctype') == 'Purchase Invoice' else "Customer"
is_purchase_invoice = self.get('doctype') == 'Purchase Invoice'
party = self.supplier if is_purchase_invoice else self.customer
party_account = self.credit_to if is_purchase_invoice else self.debit_to
party_type = "Supplier" if is_purchase_invoice else "Customer"
gain_loss_account = frappe.db.get_value('Company', self.company, 'exchange_gain_loss_account')
if not gain_loss_account:
frappe.throw(_("Please set Default Exchange Gain/Loss Account in Company {}")
.format(self.get('company')))
account_currency = get_account_currency(gain_loss_account)
if account_currency != self.company_currency:
frappe.throw(_("Currency for {0} must be {1}").format(d.account, self.company_currency))
frappe.throw(_("Currency for {0} must be {1}").format(gain_loss_account, self.company_currency))
# for purchase
dr_or_cr = 'debit' if d.exchange_gain_loss > 0 else 'credit'
# just reverse for sales?
dr_or_cr = 'debit' if dr_or_cr == 'credit' else 'credit'
if not is_purchase_invoice:
# just reverse for sales?
dr_or_cr = 'debit' if dr_or_cr == 'credit' else 'credit'
gl_entries.append(
self.get_gl_dict({
@ -904,9 +909,9 @@ class AccountsController(TransactionBase):
frappe.throw(_("Cannot overbill for Item {0} in row {1} more than {2}. To allow over-billing, please set allowance in Accounts Settings")
.format(item.item_code, item.idx, max_allowed_amt))
def get_company_default(self, fieldname):
def get_company_default(self, fieldname, ignore_validation=False):
from erpnext.accounts.utils import get_company_default
return get_company_default(self.company, fieldname)
return get_company_default(self.company, fieldname, ignore_validation=ignore_validation)
def get_stock_items(self):
stock_items = []
@ -1112,8 +1117,11 @@ class AccountsController(TransactionBase):
for d in self.get("payment_schedule"):
if d.invoice_portion:
d.payment_amount = flt(grand_total * flt(d.invoice_portion / 100), d.precision('payment_amount'))
d.base_payment_amount = flt(base_grand_total * flt(d.invoice_portion / 100), d.precision('payment_amount'))
d.base_payment_amount = flt(base_grand_total * flt(d.invoice_portion / 100), d.precision('base_payment_amount'))
d.outstanding = d.payment_amount
elif not d.invoice_portion:
d.base_payment_amount = flt(base_grand_total * self.get("conversion_rate"), d.precision('base_payment_amount'))
def set_due_date(self):
due_dates = [d.due_date for d in self.get("payment_schedule") if d.due_date]
@ -1280,6 +1288,27 @@ def validate_taxes_and_charges(tax):
tax.rate = None
def validate_account_head(tax, doc):
company = frappe.get_cached_value('Account',
tax.account_head, 'company')
if company != doc.company:
frappe.throw(_('Row {0}: Account {1} does not belong to Company {2}')
.format(tax.idx, frappe.bold(tax.account_head), frappe.bold(doc.company)), title=_('Invalid Account'))
def validate_cost_center(tax, doc):
if not tax.cost_center:
return
company = frappe.get_cached_value('Cost Center',
tax.cost_center, 'company')
if company != doc.company:
frappe.throw(_('Row {0}: Cost Center {1} does not belong to Company {2}')
.format(tax.idx, frappe.bold(tax.cost_center), frappe.bold(doc.company)), title=_('Invalid Cost Center'))
def validate_inclusive_tax(tax, doc):
def _on_previous_row_error(row_range):
throw(_("To include tax in row {0} in Item rate, taxes in rows {1} must also be included").format(tax.idx, row_range))
@ -1499,7 +1528,7 @@ def set_child_tax_template_and_map(item, child_item, parent_doc):
if child_item.get("item_tax_template"):
child_item.item_tax_rate = get_item_tax_map(parent_doc.get('company'), child_item.item_tax_template, as_json=True)
def add_taxes_from_tax_template(child_item, parent_doc):
def add_taxes_from_tax_template(child_item, parent_doc, db_insert=True):
add_taxes_from_item_tax_template = frappe.db.get_single_value("Accounts Settings", "add_taxes_from_item_tax_template")
if child_item.get("item_tax_rate") and add_taxes_from_item_tax_template:
@ -1522,7 +1551,8 @@ def add_taxes_from_tax_template(child_item, parent_doc):
"category" : "Total",
"add_deduct_tax" : "Add"
})
tax_row.db_insert()
if db_insert:
tax_row.db_insert()
def set_order_defaults(parent_doctype, parent_doctype_name, child_doctype, child_docname, trans_item):
"""

View File

@ -0,0 +1,127 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
import frappe
from frappe import _
from frappe.desk.form import assign_to
from frappe.model.document import Document
from frappe.utils import flt, unique
class EmployeeBoardingController(Document):
'''
Create the project and the task for the boarding process
Assign to the concerned person and roles as per the onboarding/separation template
'''
def validate(self):
# remove the task if linked before submitting the form
if self.amended_from:
for activity in self.activities:
activity.task = ''
def on_submit(self):
# create the project for the given employee onboarding
project_name = _(self.doctype) + ' : '
if self.doctype == 'Employee Onboarding':
project_name += self.job_applicant
else:
project_name += self.employee
project = frappe.get_doc({
'doctype': 'Project',
'project_name': project_name,
'expected_start_date': self.date_of_joining if self.doctype == 'Employee Onboarding' else self.resignation_letter_date,
'department': self.department,
'company': self.company
}).insert(ignore_permissions=True, ignore_mandatory=True)
self.db_set('project', project.name)
self.db_set('boarding_status', 'Pending')
self.reload()
self.create_task_and_notify_user()
def create_task_and_notify_user(self):
# create the task for the given project and assign to the concerned person
for activity in self.activities:
if activity.task:
continue
task = frappe.get_doc({
'doctype': 'Task',
'project': self.project,
'subject': activity.activity_name + ' : ' + self.employee_name,
'description': activity.description,
'department': self.department,
'company': self.company,
'task_weight': activity.task_weight
}).insert(ignore_permissions=True)
activity.db_set('task', task.name)
users = [activity.user] if activity.user else []
if activity.role:
user_list = frappe.db.sql_list('''
SELECT
DISTINCT(has_role.parent)
FROM
`tabHas Role` has_role
LEFT JOIN `tabUser` user
ON has_role.parent = user.name
WHERE
has_role.parenttype = 'User'
AND user.enabled = 1
AND has_role.role = %s
''', activity.role)
users = unique(users + user_list)
if 'Administrator' in users:
users.remove('Administrator')
# assign the task the users
if users:
self.assign_task_to_users(task, users)
def assign_task_to_users(self, task, users):
for user in users:
args = {
'assign_to': [user],
'doctype': task.doctype,
'name': task.name,
'description': task.description or task.subject,
'notify': self.notify_users_by_email
}
assign_to.add(args)
def on_cancel(self):
# delete task project
for task in frappe.get_all('Task', filters={'project': self.project}):
frappe.delete_doc('Task', task.name, force=1)
frappe.delete_doc('Project', self.project, force=1)
self.db_set('project', '')
for activity in self.activities:
activity.db_set('task', '')
@frappe.whitelist()
def get_onboarding_details(parent, parenttype):
return frappe.get_all('Employee Boarding Activity',
fields=['activity_name', 'role', 'user', 'required_for_employee_creation', 'description', 'task_weight'],
filters={'parent': parent, 'parenttype': parenttype},
order_by= 'idx')
def update_employee_boarding_status(project):
employee_onboarding = frappe.db.exists('Employee Onboarding', {'project': project.name})
employee_separation = frappe.db.exists('Employee Separation', {'project': project.name})
if not (employee_onboarding or employee_separation):
return
status = 'Pending'
if flt(project.percent_complete) > 0.0 and flt(project.percent_complete) < 100.0:
status = 'In Process'
elif flt(project.percent_complete) == 100.0:
status = 'Completed'
if employee_onboarding:
frappe.db.set_value('Employee Onboarding', employee_onboarding, 'boarding_status', status)
elif employee_separation:
frappe.db.set_value('Employee Separation', employee_separation, 'boarding_status', status)

View File

@ -407,6 +407,7 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters):
INNER JOIN `tabBatch` batch on sle.batch_no = batch.name
where
batch.disabled = 0
and sle.is_cancelled = 0
and sle.item_code = %(item_code)s
and sle.warehouse = %(warehouse)s
and (sle.batch_no like %(txt)s

View File

@ -27,6 +27,7 @@ class StockController(AccountsController):
if not self.get('is_return'):
self.validate_inspection()
self.validate_serialized_batch()
self.clean_serial_nos()
self.validate_customer_provided_item()
self.set_rate_of_stock_uom()
self.validate_internal_transfer()
@ -53,12 +54,17 @@ class StockController(AccountsController):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
for d in self.get("items"):
if hasattr(d, 'serial_no') and hasattr(d, 'batch_no') and d.serial_no and d.batch_no:
serial_nos = get_serial_nos(d.serial_no)
for serial_no_data in frappe.get_all("Serial No",
filters={"name": ("in", serial_nos)}, fields=["batch_no", "name"]):
if serial_no_data.batch_no != d.batch_no:
serial_nos = frappe.get_all("Serial No",
fields=["batch_no", "name", "warehouse"],
filters={
"name": ("in", get_serial_nos(d.serial_no))
}
)
for row in serial_nos:
if row.warehouse and row.batch_no != d.batch_no:
frappe.throw(_("Row #{0}: Serial No {1} does not belong to Batch {2}")
.format(d.idx, serial_no_data.name, d.batch_no))
.format(d.idx, row.name, d.batch_no))
if flt(d.qty) > 0.0 and d.get("batch_no") and self.get("posting_date") and self.docstatus < 2:
expiry_date = frappe.get_cached_value("Batch", d.get("batch_no"), "expiry_date")
@ -67,6 +73,12 @@ class StockController(AccountsController):
frappe.throw(_("Row #{0}: The batch {1} has already expired.")
.format(d.idx, get_link_to_form("Batch", d.get("batch_no"))))
def clean_serial_nos(self):
for row in self.get("items"):
if hasattr(row, "serial_no") and row.serial_no:
# replace commas by linefeed and remove all spaces in string
row.serial_no = row.serial_no.replace(",", "\n").replace(" ", "")
def get_gl_entries(self, warehouse_account=None, default_expense_account=None,
default_cost_center=None):

View File

@ -152,7 +152,7 @@ class calculate_taxes_and_totals(object):
validate_taxes_and_charges(tax)
validate_inclusive_tax(tax, self.doc)
if not self.doc.get('is_consolidated'):
if not (self.doc.get('is_consolidated') or tax.get("dont_recompute_tax")):
tax.item_wise_tax_detail = {}
tax_fields = ["total", "tax_amount_after_discount_amount",
@ -347,7 +347,7 @@ class calculate_taxes_and_totals(object):
elif tax.charge_type == "On Item Quantity":
current_tax_amount = tax_rate * item.qty
if not self.doc.get("is_consolidated"):
if not (self.doc.get("is_consolidated") or tax.get("dont_recompute_tax")):
self.set_item_wise_tax(item, tax, tax_rate, current_tax_amount)
return current_tax_amount
@ -455,7 +455,8 @@ class calculate_taxes_and_totals(object):
def _cleanup(self):
if not self.doc.get('is_consolidated'):
for tax in self.doc.get("taxes"):
tax.item_wise_tax_detail = json.dumps(tax.item_wise_tax_detail, separators=(',', ':'))
if not tax.get("dont_recompute_tax"):
tax.item_wise_tax_detail = json.dumps(tax.item_wise_tax_detail, separators=(',', ':'))
def set_discount_amount(self):
if self.doc.additional_discount_percentage:
@ -678,17 +679,13 @@ class calculate_taxes_and_totals(object):
default_mode_of_payment = frappe.db.get_value('POS Payment Method',
{'parent': self.doc.pos_profile, 'default': 1}, ['mode_of_payment'], as_dict=1)
self.doc.payments = []
if default_mode_of_payment:
self.doc.payments = []
self.doc.append('payments', {
'mode_of_payment': default_mode_of_payment.mode_of_payment,
'amount': total_amount_to_pay,
'default': 1
})
else:
self.doc.is_pos = 0
self.doc.pos_profile = ''
self.calculate_paid_amount()

View File

@ -9,7 +9,7 @@ import datetime
def create_test_lead():
test_lead = frappe.db.exists({'doctype': 'Lead', 'lead_name': 'Test Lead'})
test_lead = frappe.db.exists({'doctype': 'Lead', 'email_id':'test@example.com'})
if test_lead:
return frappe.get_doc('Lead', test_lead[0][0])
test_lead = frappe.get_doc({

View File

@ -0,0 +1,17 @@
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Campaign', {
refresh: function(frm) {
erpnext.toggle_naming_series();
if (frm.doc.__islocal) {
frm.toggle_display("naming_series", frappe.boot.sysdefaults.campaign_naming_by=="Naming Series");
} else {
cur_frm.add_custom_button(__("View Leads"), function() {
frappe.route_options = {"source": "Campaign", "campaign_name": frm.doc.name};
frappe.set_route("List", "Lead");
}, "fa fa-list", true);
}
}
});

View File

@ -1,4 +1,5 @@
{
"actions": [],
"allow_import": 1,
"allow_rename": 1,
"autoname": "naming_series:",
@ -39,17 +40,9 @@
"set_only_once": 1
},
{
"fieldname": "description",
"fieldtype": "Text",
"in_list_view": 1,
"label": "Description",
"oldfieldname": "description",
"oldfieldtype": "Text",
"width": "300px"
},
{
"fieldname": "description_section",
"fieldtype": "Section Break"
"fieldname": "campaign_schedules_section",
"fieldtype": "Section Break",
"label": "Campaign Schedules"
},
{
"fieldname": "campaign_schedules",
@ -58,16 +51,25 @@
"options": "Campaign Email Schedule"
},
{
"fieldname": "campaign_schedules_section",
"fieldtype": "Section Break",
"label": "Campaign Schedules"
"fieldname": "description_section",
"fieldtype": "Section Break"
},
{
"fieldname": "description",
"fieldtype": "Text",
"in_list_view": 1,
"label": "Description",
"oldfieldname": "description",
"oldfieldtype": "Text",
"width": "300px"
}
],
"icon": "fa fa-bullhorn",
"idx": 1,
"modified": "2019-07-22 12:03:39.832342",
"links": [],
"modified": "2021-06-30 18:05:06.412712",
"modified_by": "Administrator",
"module": "Selling",
"module": "CRM",
"name": "Campaign",
"owner": "Administrator",
"permissions": [

View File

@ -1,9 +1,7 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
from frappe.model.naming import set_name_by_naming_series

View File

@ -0,0 +1,8 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
import unittest
class TestCampaign(unittest.TestCase):
pass

View File

@ -12,7 +12,8 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller
'Opportunity': this.make_opportunity
};
this.frm.toggle_reqd("lead_name", !this.frm.doc.organization_lead);
// For avoiding integration issues.
this.frm.set_df_property('first_name', 'reqd', true);
}
onload () {
@ -42,6 +43,7 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller
if (!this.frm.is_new()) {
frappe.contacts.render_address_and_contact(this.frm);
cur_frm.trigger('render_contact_day_html');
} else {
frappe.contacts.clear_address_and_contact(this.frm);
}
@ -68,13 +70,8 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller
})
}
organization_lead () {
this.frm.toggle_reqd("lead_name", !this.frm.doc.organization_lead);
this.frm.toggle_reqd("company_name", this.frm.doc.organization_lead);
}
company_name () {
if (this.frm.doc.organization_lead && !this.frm.doc.lead_name) {
if (!this.frm.doc.lead_name) {
this.frm.set_value("lead_name", this.frm.doc.company_name);
}
}
@ -86,6 +83,19 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller
this.frm.set_value("ends_on", d.format(frappe.defaultDatetimeFormat));
}
}
render_contact_day_html() {
if (cur_frm.doc.contact_date) {
let contact_date = frappe.datetime.obj_to_str(cur_frm.doc.contact_date);
let diff_days = frappe.datetime.get_day_diff(contact_date, frappe.datetime.get_today());
let color = diff_days > 0 ? "orange" : "green";
let message = diff_days > 0 ? __("Next Contact Date") : __("Last Contact Date");
let html = `<div class="col-xs-12">
<span class="indicator whitespace-nowrap ${color}"><span> ${message} : ${frappe.datetime.global_date_format(contact_date)}</span></span>
</div>` ;
cur_frm.dashboard.set_headline_alert(html);
}
}
};
extend_cscript(cur_frm.cscript, new erpnext.LeadController({ frm: cur_frm }));

View File

@ -9,71 +9,70 @@
"email_append_to": 1,
"engine": "InnoDB",
"field_order": [
"organization_lead",
"lead_details",
"naming_series",
"lead_name",
"company_name",
"email_id",
"col_break123",
"lead_owner",
"status",
"salutation",
"first_name",
"middle_name",
"last_name",
"lead_name",
"col_break123",
"status",
"company_name",
"designation",
"gender",
"source",
"customer",
"campaign_name",
"image",
"section_break_12",
"contact_by",
"column_break_14",
"contact_date",
"ends_on",
"notes_section",
"notes",
"address_info",
"contact_details_section",
"email_id",
"mobile_no",
"whatsapp_no",
"column_break_16",
"phone",
"phone_ext",
"additional_information_section",
"no_of_employees",
"industry",
"market_segment",
"column_break_22",
"fax",
"website",
"type",
"request_type",
"address_section",
"address_html",
"address_type",
"address_title",
"address_line1",
"address_line2",
"city",
"pincode",
"county",
"column_break2",
"contact_html",
"state",
"country",
"pincode",
"contact_section",
"phone",
"mobile_no",
"fax",
"website",
"more_info",
"type",
"market_segment",
"industry",
"request_type",
"column_break3",
"section_break_12",
"lead_owner",
"ends_on",
"column_break_14",
"contact_by",
"contact_date",
"lead_source_details_section",
"company",
"territory",
"language",
"column_break_50",
"source",
"campaign_name",
"unsubscribed",
"blog_subscriber",
"notes_section",
"notes",
"other_information_section",
"customer",
"image",
"title"
],
"fields": [
{
"default": "0",
"fieldname": "organization_lead",
"fieldtype": "Check",
"label": "Lead is an Organization",
"set_only_once": 1
},
{
"fieldname": "lead_details",
"fieldtype": "Section Break",
"label": "Lead Details",
"options": "fa fa-user"
},
{
@ -90,16 +89,19 @@
"fieldname": "lead_name",
"fieldtype": "Data",
"in_global_search": 1,
"label": "Person Name",
"label": "Full Name",
"oldfieldname": "lead_name",
"oldfieldtype": "Data",
"read_only": 1,
"search_index": 1
},
{
"fieldname": "company_name",
"fieldtype": "Data",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Organization Name",
"mandatory_depends_on": "eval: !(doc.first_name)",
"oldfieldname": "company_name",
"oldfieldtype": "Data"
},
@ -121,7 +123,6 @@
"default": "__user",
"fieldname": "lead_owner",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Lead Owner",
"oldfieldname": "lead_owner",
"oldfieldtype": "Link",
@ -143,7 +144,6 @@
"search_index": 1
},
{
"depends_on": "eval: doc.__islocal",
"fieldname": "salutation",
"fieldtype": "Link",
"label": "Salutation",
@ -241,46 +241,22 @@
"read_only": 1
},
{
"depends_on": "eval: doc.__islocal",
"description": "Home, Work, etc.",
"fieldname": "address_title",
"fieldtype": "Data",
"label": "Address Title"
},
{
"depends_on": "eval: doc.__islocal",
"fieldname": "address_line1",
"fieldtype": "Data",
"label": "Address Line 1",
"mandatory_depends_on": "eval: doc.address_title && doc.address_type"
},
{
"depends_on": "eval: doc.__islocal",
"fieldname": "address_line2",
"fieldtype": "Data",
"label": "Address Line 2"
},
{
"depends_on": "eval: doc.__islocal",
"fieldname": "city",
"fieldtype": "Data",
"label": "City/Town",
"mandatory_depends_on": "eval: doc.address_title && doc.address_type"
},
{
"depends_on": "eval: doc.__islocal",
"fieldname": "county",
"fieldtype": "Data",
"label": "County"
},
{
"depends_on": "eval: doc.__islocal",
"fieldname": "state",
"fieldtype": "Data",
"label": "State"
},
{
"depends_on": "eval: doc.__islocal",
"fieldname": "country",
"fieldtype": "Link",
"label": "Country",
@ -288,7 +264,6 @@
"options": "Country"
},
{
"depends_on": "eval: doc.__islocal",
"fieldname": "pincode",
"fieldtype": "Data",
"label": "Postal Code"
@ -304,7 +279,6 @@
"read_only": 1
},
{
"depends_on": "eval: doc.__islocal",
"fieldname": "phone",
"fieldtype": "Data",
"label": "Phone",
@ -313,7 +287,6 @@
"options": "Phone"
},
{
"depends_on": "eval: doc.__islocal",
"fieldname": "mobile_no",
"fieldtype": "Data",
"label": "Mobile No.",
@ -322,21 +295,12 @@
"options": "Phone"
},
{
"depends_on": "eval: doc.__islocal",
"fieldname": "fax",
"fieldtype": "Data",
"label": "Fax",
"oldfieldname": "fax",
"oldfieldtype": "Data"
},
{
"collapsible": 1,
"fieldname": "more_info",
"fieldtype": "Section Break",
"label": "More Information",
"oldfieldtype": "Section Break",
"options": "fa fa-file-text"
},
{
"fieldname": "type",
"fieldtype": "Select",
@ -369,12 +333,6 @@
"oldfieldtype": "Select",
"options": "\nProduct Enquiry\nRequest for Information\nSuggestions\nOther"
},
{
"fieldname": "column_break3",
"fieldtype": "Column Break",
"oldfieldtype": "Column Break",
"width": "50%"
},
{
"fieldname": "company",
"fieldtype": "Link",
@ -389,11 +347,14 @@
"fieldtype": "Data",
"label": "Website",
"oldfieldname": "website",
"oldfieldtype": "Data"
"oldfieldtype": "Data",
"options": "URL"
},
{
"fieldname": "territory",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Territory",
"oldfieldname": "territory",
"oldfieldtype": "Link",
@ -422,45 +383,95 @@
{
"fieldname": "designation",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Designation",
"options": "Designation"
},
{
"collapsible": 1,
"collapsible_depends_on": "eval: doc.__islocal",
"fieldname": "address_info",
"fieldtype": "Section Break",
"label": "Address & Contact",
"oldfieldtype": "Column Break",
"options": "fa fa-map-marker"
},
{
"collapsible": 1,
"collapsible_depends_on": "eval: doc.__islocal",
"fieldname": "contact_section",
"fieldtype": "Section Break",
"label": "Contact"
},
{
"default": "Billing",
"depends_on": "eval: doc.__islocal",
"fieldname": "address_type",
"fieldtype": "Select",
"label": "Address Type",
"options": "Billing\nShipping\nOffice\nPersonal\nPlant\nPostal\nShop\nSubsidiary\nWarehouse\nCurrent\nPermanent\nOther"
},
{
"fieldname": "language",
"fieldtype": "Link",
"label": "Print Language",
"options": "Language"
},
{
"fieldname": "first_name",
"fieldtype": "Data",
"label": "First Name",
"mandatory_depends_on": "eval: !(doc.company_name)"
},
{
"fieldname": "middle_name",
"fieldtype": "Data",
"label": "Middle Name"
},
{
"fieldname": "last_name",
"fieldtype": "Data",
"label": "Last Name"
},
{
"collapsible": 1,
"fieldname": "additional_information_section",
"fieldtype": "Section Break",
"label": "Additional Information"
},
{
"fieldname": "no_of_employees",
"fieldtype": "Int",
"label": "No. of Employees"
},
{
"fieldname": "column_break_22",
"fieldtype": "Column Break"
},
{
"fieldname": "whatsapp_no",
"fieldtype": "Data",
"label": "WhatsApp No.",
"options": "Phone"
},
{
"collapsible": 1,
"depends_on": "eval: !doc.__islocal",
"fieldname": "address_section",
"fieldtype": "Section Break",
"label": "Address"
},
{
"fieldname": "lead_source_details_section",
"fieldtype": "Section Break",
"label": "Lead Source Details"
},
{
"fieldname": "column_break_50",
"fieldtype": "Column Break"
},
{
"fieldname": "other_information_section",
"fieldtype": "Section Break",
"label": "Other Information"
},
{
"fieldname": "contact_details_section",
"fieldtype": "Section Break",
"label": "Contact Details"
},
{
"fieldname": "column_break_16",
"fieldtype": "Column Break"
},
{
"fieldname": "phone_ext",
"fieldtype": "Data",
"label": "Phone Ext."
}
],
"icon": "fa fa-user",
"idx": 5,
"image_field": "image",
"links": [],
"modified": "2021-01-06 19:39:58.748978",
"modified": "2021-08-04 00:24:57.208590",
"modified_by": "Administrator",
"module": "CRM",
"name": "Lead",

View File

@ -21,26 +21,24 @@ class Lead(SellingController):
self.get("__onload").is_customer = customer
load_address_and_contact(self)
def before_insert(self):
if self.address_title and self.address_type:
self.address_doc = self.create_address()
self.contact_doc = self.create_contact()
def after_insert(self):
self.update_links()
def validate(self):
self.set_full_name()
self.set_lead_name()
self.set_title()
self.set_status()
self.check_email_id_is_unique()
self.validate_email_id()
self.validate_contact_date()
self._prev = frappe._dict({
"contact_date": frappe.db.get_value("Lead", self.name, "contact_date") if (not cint(self.is_new())) else None,
"ends_on": frappe.db.get_value("Lead", self.name, "ends_on") if (not cint(self.is_new())) else None,
"contact_by": frappe.db.get_value("Lead", self.name, "contact_by") if (not cint(self.is_new())) else None,
})
def set_full_name(self):
self.lead_name = " ".join(filter(None, [self.first_name, self.middle_name, self.last_name]))
self.set_status()
self.check_email_id_is_unique()
def validate_email_id(self):
if self.email_id:
if not self.flags.ignore_email_validation:
validate_email_address(self.email_id, throw=True)
@ -54,6 +52,7 @@ class Lead(SellingController):
if self.is_new() or not self.image:
self.image = has_gravatar(self.email_id)
def validate_contact_date(self):
if self.contact_date and getdate(self.contact_date) < getdate(nowdate()):
frappe.throw(_("Next Contact Date cannot be in the past"))
@ -64,6 +63,22 @@ class Lead(SellingController):
def on_update(self):
self.add_calendar_event()
def before_insert(self):
self.contact_doc = self.create_contact()
def after_insert(self):
self.update_links()
def update_links(self):
# update contact links
if self.contact_doc:
self.contact_doc.append("links", {
"link_doctype": "Lead",
"link_name": self.name,
"link_title": self.lead_name
})
self.contact_doc.save()
def add_calendar_event(self, opts=None, force=False):
super(Lead, self).add_calendar_event({
"owner": self.lead_owner,
@ -86,8 +101,26 @@ class Lead(SellingController):
def on_trash(self):
frappe.db.sql("""update `tabIssue` set lead='' where lead=%s""", self.name)
self.unlink_dynamic_links()
self.delete_events()
def unlink_dynamic_links(self):
links = frappe.get_all('Dynamic Link', filters={'link_doctype': self.doctype, 'link_name': self.name}, fields=['parent', 'parenttype'])
for link in links:
linked_doc = frappe.get_doc(link['parenttype'], link['parent'])
if len(linked_doc.get('links')) == 1:
linked_doc.delete(ignore_permissions=True)
else:
to_remove = None
for d in linked_doc.get('links'):
if d.link_doctype == self.doctype and d.link_name == self.name:
to_remove = d
if to_remove:
linked_doc.remove(to_remove)
linked_doc.save(ignore_permissions=True)
def has_customer(self):
return frappe.db.get_value("Customer", {"lead_name": self.name})
@ -99,7 +132,6 @@ class Lead(SellingController):
"party_name": self.name,
"docstatus": 1,
"status": ["!=", "Lost"]
})
def has_lost_quotation(self):
@ -120,40 +152,17 @@ class Lead(SellingController):
self.lead_name = self.email_id.split("@")[0]
def set_title(self):
if self.organization_lead:
self.title = self.company_name
else:
self.title = self.lead_name
def create_address(self):
address_fields = ["address_type", "address_title", "address_line1", "address_line2",
"city", "county", "state", "country", "pincode"]
info_fields = ["email_id", "phone", "fax"]
# do not create an address if no fields are available,
# skipping country since the system auto-sets it from system defaults
address = frappe.new_doc("Address")
address.update({addr_field: self.get(addr_field) for addr_field in address_fields})
address.update({info_field: self.get(info_field) for info_field in info_fields})
address.insert()
return address
self.title = self.company_name or self.lead_name
def create_contact(self):
if not self.lead_name:
self.set_full_name()
self.set_lead_name()
names = self.lead_name.strip().split(" ")
if len(names) > 1:
first_name, last_name = names[0], " ".join(names[1:])
else:
first_name, last_name = self.lead_name, None
contact = frappe.new_doc("Contact")
contact.update({
"first_name": first_name,
"last_name": last_name,
"first_name": self.first_name or self.lead_name,
"last_name": self.last_name,
"salutation": self.salutation,
"gender": self.gender,
"designation": self.designation,
@ -181,25 +190,6 @@ class Lead(SellingController):
return contact
def update_links(self):
# update address links
if hasattr(self, 'address_doc'):
self.address_doc.append("links", {
"link_doctype": "Lead",
"link_name": self.name,
"link_title": self.lead_name
})
self.address_doc.save()
# update contact links
if self.contact_doc:
self.contact_doc.append("links", {
"link_doctype": "Lead",
"link_name": self.name,
"link_title": self.lead_name
})
self.contact_doc.save()
@frappe.whitelist()
def make_customer(source_name, target_doc=None):
return _make_customer(source_name, target_doc)

View File

@ -4,6 +4,7 @@
from __future__ import unicode_literals
import frappe
from frappe.utils import random_string
import unittest
test_records = frappe.get_test_records('Lead')
@ -32,3 +33,53 @@ class TestLead(unittest.TestCase):
customer.company = "_Test Company"
customer.customer_group = "_Test Customer Group"
customer.insert()
def test_create_lead_and_unlinking_dynamic_links(self):
lead_doc = make_lead(first_name = "Lorem", last_name="Ipsum", email_id="lorem_ipsum@example.com")
lead_doc_1 = make_lead()
frappe.get_doc({
"doctype": "Address",
"address_type": "Billing",
"city": "Mumbai",
"address_line1": "Vidya Vihar West",
"country": "India",
"links": [{
"link_doctype": "Lead",
"link_name": lead_doc.name
}]
}).insert()
address_1 = frappe.get_doc({
"doctype": "Address",
"address_type": "Billing",
"address_line1": "Baner",
"city": "Pune",
"country": "India",
"links": [
{
"link_doctype": "Lead",
"link_name": lead_doc.name
},
{
"link_doctype": "Lead",
"link_name": lead_doc_1.name
}
]
}).insert()
lead_doc.delete()
address_1.reload()
self.assertEqual(frappe.db.exists("Lead",lead_doc.name), None)
self.assertEqual(len(address_1.get('links')), 1)
def make_lead(**args):
args = frappe._dict(args)
lead_doc = frappe.get_doc({
"doctype": "Lead",
"first_name": args.first_name or "_Test",
"last_name": args.last_name or "Lead",
"email_id": args.email_id or "new_lead_{}@example.com".format(random_string(5)),
}).insert()
return lead_doc

View File

@ -27,7 +27,6 @@
{
"doctype": "Lead",
"email_id": "test_lead4@example.com",
"organization_lead": 1,
"lead_name": "_Test Lead 4",
"company_name": "_Test Lead 4",
"status": "Open"

View File

@ -9,7 +9,6 @@ QUnit.test("test: lead", function (assert) {
() => frappe.set_route("List", "Lead"),
() => frappe.new_doc("Lead"),
() => frappe.timeout(1),
() => cur_frm.set_value("organization_lead", "1"),
() => cur_frm.set_value("company_name", lead_name),
() => cur_frm.save(),
() => frappe.timeout(1),

View File

@ -53,6 +53,13 @@ frappe.ui.form.on("Opportunity", {
frm.get_field("items").grid.set_multiple_add("item_code", "qty");
},
status:function(frm){
if (frm.doc.status == "Lost"){
frm.trigger('set_as_lost_dialog');
}
},
customer_address: function(frm, cdt, cdn) {
erpnext.utils.get_address_display(frm, 'customer_address', 'address_display', false);
},
@ -91,11 +98,6 @@ frappe.ui.form.on("Opportunity", {
frm.add_custom_button(__('Quotation'),
cur_frm.cscript.create_quotation, __('Create'));
if(doc.status!=="Quotation") {
frm.add_custom_button(__('Lost'), () => {
frm.trigger('set_as_lost_dialog');
});
}
}
if(!frm.doc.__islocal && frm.perm[0].write && frm.doc.docstatus==0) {

View File

@ -34,11 +34,14 @@ def enroll_student(source_name):
}
}}, ignore_permissions=True)
student.save()
student_applicant = frappe.db.get_value("Student Applicant", source_name,
["student_category", "program"], as_dict=True)
program_enrollment = frappe.new_doc("Program Enrollment")
program_enrollment.student = student.name
program_enrollment.student_category = student.student_category
program_enrollment.student_category = student_applicant.student_category
program_enrollment.student_name = student.title
program_enrollment.program = frappe.db.get_value("Student Applicant", source_name, "program")
program_enrollment.program = student_applicant.program
frappe.publish_realtime('enroll_student_progress', {"progress": [2, 4]}, user=frappe.session.user)
return program_enrollment

View File

@ -1,195 +1,68 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2016-06-10 03:29:02.539914",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"actions": [],
"creation": "2016-06-10 03:29:02.539914",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"student_applicant",
"student",
"student_name",
"column_break_3",
"student_batch_name",
"student_category"
],
"fields": [
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "",
"fieldname": "student_applicant",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Student Applicant",
"length": 0,
"no_copy": 0,
"options": "Student Applicant",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"fieldname": "student_applicant",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Student Applicant",
"options": "Student Applicant"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "",
"fieldname": "student",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Student",
"length": 0,
"no_copy": 0,
"options": "Student",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"fieldname": "student",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Student",
"options": "Student"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_3",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "student_name",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Student Name",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"fieldname": "student_name",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Student Name",
"read_only": 1
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "student_batch_name",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Student Batch Name",
"length": 0,
"no_copy": 0,
"options": "Student Batch Name",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
"fieldname": "student_batch_name",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Student Batch Name",
"options": "Student Batch Name"
},
{
"fieldname": "student_category",
"fieldtype": "Link",
"label": "Student Category",
"options": "Student Category",
"read_only": 1
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2018-01-02 12:03:53.890741",
"modified_by": "Administrator",
"module": "Education",
"name": "Program Enrollment Tool Student",
"name_case": "",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"restrict_to_domain": "Education",
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 0,
"track_seen": 0
],
"istable": 1,
"links": [],
"modified": "2021-07-29 18:19:54.471594",
"modified_by": "Administrator",
"module": "Education",
"name": "Program Enrollment Tool Student",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"restrict_to_domain": "Education",
"sort_field": "modified",
"sort_order": "DESC"
}

View File

@ -1,353 +0,0 @@
from __future__ import unicode_literals
import frappe
from frappe import _
import json
from frappe.utils import cstr, cint, nowdate, getdate, flt, get_request_session, get_datetime
from erpnext.erpnext_integrations.utils import validate_webhooks_request
from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note, make_sales_invoice
from erpnext.erpnext_integrations.doctype.shopify_settings.sync_product import sync_item_from_shopify
from erpnext.erpnext_integrations.doctype.shopify_settings.sync_customer import create_customer
from erpnext.erpnext_integrations.doctype.shopify_log.shopify_log import make_shopify_log, dump_request_data
from erpnext.erpnext_integrations.doctype.shopify_settings.shopify_settings import get_shopify_url, get_header
@frappe.whitelist(allow_guest=True)
@validate_webhooks_request("Shopify Settings", 'X-Shopify-Hmac-Sha256', secret_key='shared_secret')
def store_request_data(order=None, event=None):
if frappe.request:
order = json.loads(frappe.request.data)
event = frappe.request.headers.get('X-Shopify-Topic')
dump_request_data(order, event)
def sync_sales_order(order, request_id=None, old_order_sync=False):
frappe.set_user('Administrator')
shopify_settings = frappe.get_doc("Shopify Settings")
frappe.flags.request_id = request_id
if not frappe.db.get_value("Sales Order", filters={"shopify_order_id": cstr(order['id'])}):
try:
validate_customer(order, shopify_settings)
validate_item(order, shopify_settings)
create_order(order, shopify_settings, old_order_sync=old_order_sync)
except Exception as e:
make_shopify_log(status="Error", exception=e)
else:
make_shopify_log(status="Success")
def prepare_sales_invoice(order, request_id=None):
frappe.set_user('Administrator')
shopify_settings = frappe.get_doc("Shopify Settings")
frappe.flags.request_id = request_id
try:
sales_order = get_sales_order(cstr(order['id']))
if sales_order:
create_sales_invoice(order, shopify_settings, sales_order)
make_shopify_log(status="Success")
except Exception as e:
make_shopify_log(status="Error", exception=e, rollback=True)
def prepare_delivery_note(order, request_id=None):
frappe.set_user('Administrator')
shopify_settings = frappe.get_doc("Shopify Settings")
frappe.flags.request_id = request_id
try:
sales_order = get_sales_order(cstr(order['id']))
if sales_order:
create_delivery_note(order, shopify_settings, sales_order)
make_shopify_log(status="Success")
except Exception as e:
make_shopify_log(status="Error", exception=e, rollback=True)
def get_sales_order(shopify_order_id):
sales_order = frappe.db.get_value("Sales Order", filters={"shopify_order_id": shopify_order_id})
if sales_order:
so = frappe.get_doc("Sales Order", sales_order)
return so
def validate_customer(order, shopify_settings):
customer_id = order.get("customer", {}).get("id")
if customer_id:
if not frappe.db.get_value("Customer", {"shopify_customer_id": customer_id}, "name"):
create_customer(order.get("customer"), shopify_settings)
def validate_item(order, shopify_settings):
for item in order.get("line_items"):
if item.get("product_id") and not frappe.db.get_value("Item", {"shopify_product_id": item.get("product_id")}, "name"):
sync_item_from_shopify(shopify_settings, item)
def create_order(order, shopify_settings, old_order_sync=False, company=None):
so = create_sales_order(order, shopify_settings, company)
if so:
if order.get("financial_status") == "paid":
create_sales_invoice(order, shopify_settings, so, old_order_sync=old_order_sync)
if order.get("fulfillments") and not old_order_sync:
create_delivery_note(order, shopify_settings, so)
def create_sales_order(shopify_order, shopify_settings, company=None):
product_not_exists = []
customer = frappe.db.get_value("Customer", {"shopify_customer_id": shopify_order.get("customer", {}).get("id")}, "name")
so = frappe.db.get_value("Sales Order", {"shopify_order_id": shopify_order.get("id")}, "name")
if not so:
items = get_order_items(shopify_order.get("line_items"), shopify_settings, getdate(shopify_order.get('created_at')))
if not items:
message = 'Following items exists in the shopify order but relevant records were not found in the shopify Product master'
message += "\n" + ", ".join(product_not_exists)
make_shopify_log(status="Error", exception=message, rollback=True)
return ''
so = frappe.get_doc({
"doctype": "Sales Order",
"naming_series": shopify_settings.sales_order_series or "SO-Shopify-",
"shopify_order_id": shopify_order.get("id"),
"shopify_order_number": shopify_order.get("name"),
"customer": customer or shopify_settings.default_customer,
"transaction_date": getdate(shopify_order.get("created_at")) or nowdate(),
"delivery_date": getdate(shopify_order.get("created_at")) or nowdate(),
"company": shopify_settings.company,
"selling_price_list": shopify_settings.price_list,
"ignore_pricing_rule": 1,
"items": items,
"taxes": get_order_taxes(shopify_order, shopify_settings),
"apply_discount_on": "Grand Total",
"discount_amount": get_discounted_amount(shopify_order),
})
if company:
so.update({
"company": company,
"status": "Draft"
})
so.flags.ignore_mandatory = True
so.save(ignore_permissions=True)
so.submit()
else:
so = frappe.get_doc("Sales Order", so)
frappe.db.commit()
return so
def create_sales_invoice(shopify_order, shopify_settings, so, old_order_sync=False):
if not frappe.db.get_value("Sales Invoice", {"shopify_order_id": shopify_order.get("id")}, "name")\
and so.docstatus==1 and not so.per_billed and cint(shopify_settings.sync_sales_invoice):
if old_order_sync:
posting_date = getdate(shopify_order.get('created_at'))
else:
posting_date = nowdate()
si = make_sales_invoice(so.name, ignore_permissions=True)
si.shopify_order_id = shopify_order.get("id")
si.shopify_order_number = shopify_order.get("name")
si.set_posting_time = 1
si.posting_date = posting_date
si.due_date = posting_date
si.naming_series = shopify_settings.sales_invoice_series or "SI-Shopify-"
si.flags.ignore_mandatory = True
set_cost_center(si.items, shopify_settings.cost_center)
si.insert(ignore_mandatory=True)
si.submit()
make_payament_entry_against_sales_invoice(si, shopify_settings, posting_date)
frappe.db.commit()
def set_cost_center(items, cost_center):
for item in items:
item.cost_center = cost_center
def make_payament_entry_against_sales_invoice(doc, shopify_settings, posting_date=None):
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
payment_entry = get_payment_entry(doc.doctype, doc.name, bank_account=shopify_settings.cash_bank_account)
payment_entry.flags.ignore_mandatory = True
payment_entry.reference_no = doc.name
payment_entry.posting_date = posting_date or nowdate()
payment_entry.reference_date = posting_date or nowdate()
payment_entry.insert(ignore_permissions=True)
payment_entry.submit()
def create_delivery_note(shopify_order, shopify_settings, so):
if not cint(shopify_settings.sync_delivery_note):
return
for fulfillment in shopify_order.get("fulfillments"):
if not frappe.db.get_value("Delivery Note", {"shopify_fulfillment_id": fulfillment.get("id")}, "name")\
and so.docstatus==1:
dn = make_delivery_note(so.name)
dn.shopify_order_id = fulfillment.get("order_id")
dn.shopify_order_number = shopify_order.get("name")
dn.set_posting_time = 1
dn.posting_date = getdate(fulfillment.get("created_at"))
dn.shopify_fulfillment_id = fulfillment.get("id")
dn.naming_series = shopify_settings.delivery_note_series or "DN-Shopify-"
dn.items = get_fulfillment_items(dn.items, fulfillment.get("line_items"), shopify_settings)
dn.flags.ignore_mandatory = True
dn.save()
dn.submit()
frappe.db.commit()
def get_fulfillment_items(dn_items, fulfillment_items, shopify_settings):
return [dn_item.update({"qty": item.get("quantity")}) for item in fulfillment_items for dn_item in dn_items\
if get_item_code(item) == dn_item.item_code]
def get_discounted_amount(order):
discounted_amount = 0.0
for discount in order.get("discount_codes"):
discounted_amount += flt(discount.get("amount"))
return discounted_amount
def get_order_items(order_items, shopify_settings, delivery_date):
items = []
all_product_exists = True
product_not_exists = []
for shopify_item in order_items:
if not shopify_item.get('product_exists'):
all_product_exists = False
product_not_exists.append({'title':shopify_item.get('title'),
'shopify_order_id': shopify_item.get('id')})
continue
if all_product_exists:
item_code = get_item_code(shopify_item)
items.append({
"item_code": item_code,
"item_name": shopify_item.get("name"),
"rate": shopify_item.get("price"),
"delivery_date": delivery_date,
"qty": shopify_item.get("quantity"),
"stock_uom": shopify_item.get("uom") or _("Nos"),
"warehouse": shopify_settings.warehouse
})
else:
items = []
return items
def get_item_code(shopify_item):
item_code = frappe.db.get_value("Item", {"shopify_variant_id": shopify_item.get("variant_id")}, "item_code")
if not item_code:
item_code = frappe.db.get_value("Item", {"shopify_product_id": shopify_item.get("product_id")}, "item_code")
if not item_code:
item_code = frappe.db.get_value("Item", {"item_name": shopify_item.get("title")}, "item_code")
return item_code
def get_order_taxes(shopify_order, shopify_settings):
taxes = []
for tax in shopify_order.get("tax_lines"):
taxes.append({
"charge_type": _("On Net Total"),
"account_head": get_tax_account_head(tax),
"description": "{0} - {1}%".format(tax.get("title"), tax.get("rate") * 100.0),
"rate": tax.get("rate") * 100.00,
"included_in_print_rate": 1 if shopify_order.get("taxes_included") else 0,
"cost_center": shopify_settings.cost_center
})
taxes = update_taxes_with_shipping_lines(taxes, shopify_order.get("shipping_lines"), shopify_settings)
return taxes
def update_taxes_with_shipping_lines(taxes, shipping_lines, shopify_settings):
"""Shipping lines represents the shipping details,
each such shipping detail consists of a list of tax_lines"""
for shipping_charge in shipping_lines:
if shipping_charge.get("price"):
taxes.append({
"charge_type": _("Actual"),
"account_head": get_tax_account_head(shipping_charge),
"description": shipping_charge["title"],
"tax_amount": shipping_charge["price"],
"cost_center": shopify_settings.cost_center
})
for tax in shipping_charge.get("tax_lines"):
taxes.append({
"charge_type": _("Actual"),
"account_head": get_tax_account_head(tax),
"description": tax["title"],
"tax_amount": tax["price"],
"cost_center": shopify_settings.cost_center
})
return taxes
def get_tax_account_head(tax):
tax_title = tax.get("title").encode("utf-8")
tax_account = frappe.db.get_value("Shopify Tax Account", \
{"parent": "Shopify Settings", "shopify_tax": tax_title}, "tax_account")
if not tax_account:
frappe.throw(_("Tax Account not specified for Shopify Tax {0}").format(tax.get("title")))
return tax_account
@frappe.whitelist(allow_guest=True)
def sync_old_orders():
frappe.set_user('Administrator')
shopify_settings = frappe.get_doc('Shopify Settings')
if not shopify_settings.sync_missing_orders:
return
url = get_url(shopify_settings)
session = get_request_session()
try:
res = session.get(url, headers=get_header(shopify_settings))
res.raise_for_status()
orders = res.json()["orders"]
for order in orders:
if is_sync_complete(shopify_settings, order):
stop_sync(shopify_settings)
return
sync_sales_order(order=order, old_order_sync=True)
last_order_id = order.get('id')
if last_order_id:
shopify_settings.load_from_db()
shopify_settings.last_order_id = last_order_id
shopify_settings.save()
frappe.db.commit()
except Exception as e:
raise e
def stop_sync(shopify_settings):
shopify_settings.sync_missing_orders = 0
shopify_settings.last_order_id = ''
shopify_settings.save()
frappe.db.commit()
def get_url(shopify_settings):
last_order_id = shopify_settings.last_order_id
if not last_order_id:
if shopify_settings.sync_based_on == 'Date':
url = get_shopify_url("admin/api/2021-04/orders.json?limit=250&created_at_min={0}&since_id=0".format(
get_datetime(shopify_settings.from_date)), shopify_settings)
else:
url = get_shopify_url("admin/api/2021-04/orders.json?limit=250&since_id={0}".format(
shopify_settings.from_order_id), shopify_settings)
else:
url = get_shopify_url("admin/api/2021-04/orders.json?limit=250&since_id={0}".format(last_order_id), shopify_settings)
return url
def is_sync_complete(shopify_settings, order):
if shopify_settings.sync_based_on == 'Date':
return getdate(shopify_settings.to_date) < getdate(order.get('created_at'))
else:
return cstr(order.get('id')) == cstr(shopify_settings.to_order_id)

View File

@ -1,22 +0,0 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Shopify Log', {
refresh: function(frm) {
if (frm.doc.request_data && frm.doc.status=='Error'){
frm.add_custom_button('Resync', function() {
frappe.call({
method:"erpnext.erpnext_integrations.doctype.shopify_log.shopify_log.resync",
args:{
method:frm.doc.method,
name: frm.doc.name,
request_data: frm.doc.request_data
},
callback: function(r){
frappe.msgprint(__("Order rescheduled for sync"))
}
})
}).addClass('btn-primary');
}
}
});

View File

@ -1,268 +0,0 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2016-03-14 10:02:06.227184",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "System",
"editable_grid": 0,
"fields": [
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "title",
"fieldtype": "Data",
"hidden": 1,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Title",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "Queued",
"fieldname": "status",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Status",
"length": 0,
"no_copy": 0,
"options": "",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "method",
"fieldtype": "Small Text",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Method",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "message",
"fieldtype": "Code",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Message",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "traceback",
"fieldtype": "Code",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Traceback",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "request_data",
"fieldtype": "Code",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Request Data",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 1,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2018-04-20 16:23:36.862381",
"modified_by": "Administrator",
"module": "ERPNext Integrations",
"name": "Shopify Log",
"name_case": "",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "Administrator",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
},
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
}
],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "title",
"track_changes": 0,
"track_seen": 0
}

View File

@ -1,68 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
import json
from frappe.model.document import Document
from erpnext.erpnext_integrations.utils import get_webhook_address
class ShopifyLog(Document):
pass
def make_shopify_log(status="Queued", exception=None, rollback=False):
# if name not provided by log calling method then fetch existing queued state log
make_new = False
if not frappe.flags.request_id:
make_new = True
if rollback:
frappe.db.rollback()
if make_new:
log = frappe.get_doc({"doctype":"Shopify Log"}).insert(ignore_permissions=True)
else:
log = log = frappe.get_doc("Shopify Log", frappe.flags.request_id)
log.message = get_message(exception)
log.traceback = frappe.get_traceback()
log.status = status
log.save(ignore_permissions=True)
frappe.db.commit()
def get_message(exception):
message = None
if hasattr(exception, 'message'):
message = exception.message
elif hasattr(exception, '__str__'):
message = exception.__str__()
else:
message = "Something went wrong while syncing"
return message
def dump_request_data(data, event="create/order"):
event_mapper = {
"orders/create": get_webhook_address(connector_name='shopify_connection', method="sync_sales_order", exclude_uri=True),
"orders/paid" : get_webhook_address(connector_name='shopify_connection', method="prepare_sales_invoice", exclude_uri=True),
"orders/fulfilled": get_webhook_address(connector_name='shopify_connection', method="prepare_delivery_note", exclude_uri=True)
}
log = frappe.get_doc({
"doctype": "Shopify Log",
"request_data": json.dumps(data, indent=1),
"method": event_mapper[event]
}).insert(ignore_permissions=True)
frappe.db.commit()
frappe.enqueue(method=event_mapper[event], queue='short', timeout=300, is_async=True,
**{"order": data, "request_id": log.name})
@frappe.whitelist()
def resync(method, name, request_data):
frappe.db.set_value("Shopify Log", name, "status", "Queued", update_modified=False)
frappe.enqueue(method=method, queue='short', timeout=300, is_async=True,
**{"order": json.loads(request_data), "request_id": name})

View File

@ -1,12 +0,0 @@
frappe.listview_settings['Shopify Log'] = {
add_fields: ["status"],
get_indicator: function(doc) {
if(doc.status==="Success"){
return [__("Success"), "green", "status,=,Success"];
} else if(doc.status ==="Error"){
return [__("Error"), "red", "status,=,Error"];
} else if(doc.status ==="Queued"){
return [__("Queued"), "orange", "status,=,Queued"];
}
}
}

View File

@ -1,23 +0,0 @@
/* eslint-disable */
// rename this file from _test_[name] to test_[name] to activate
// and remove above this line
QUnit.test("test: Shopify Log", function (assert) {
let done = assert.async();
// number of asserts
assert.expect(1);
frappe.run_serially([
// insert a new Shopify Log
() => frappe.tests.make('Shopify Log', [
// values to be set
{key: 'value'}
]),
() => {
assert.equal(cur_frm.doc.key, 'value');
},
() => done()
]);
});

View File

@ -1,12 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
import frappe
import unittest
# test_records = frappe.get_test_records('Shopify Log')
class TestShopifyLog(unittest.TestCase):
pass

View File

@ -1,90 +0,0 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
frappe.provide("erpnext_integrations.shopify_settings");
frappe.ui.form.on("Shopify Settings", "onload", function(frm){
frappe.call({
method:"erpnext.erpnext_integrations.doctype.shopify_settings.shopify_settings.get_series",
callback:function(r){
$.each(r.message, function(key, value){
set_field_options(key, value);
});
}
});
erpnext_integrations.shopify_settings.setup_queries(frm);
})
frappe.ui.form.on("Shopify Settings", "app_type", function(frm) {
frm.toggle_reqd("api_key", (frm.doc.app_type == "Private"));
frm.toggle_reqd("password", (frm.doc.app_type == "Private"));
})
frappe.ui.form.on("Shopify Settings", "refresh", function(frm){
if(!frm.doc.__islocal && frm.doc.enable_shopify === 1){
frm.toggle_reqd("price_list", true);
frm.toggle_reqd("warehouse", true);
frm.toggle_reqd("taxes", true);
frm.toggle_reqd("company", true);
frm.toggle_reqd("cost_center", true);
frm.toggle_reqd("cash_bank_account", true);
frm.toggle_reqd("sales_order_series", true);
frm.toggle_reqd("customer_group", true);
frm.toggle_reqd("shared_secret", true);
frm.toggle_reqd("sales_invoice_series", frm.doc.sync_sales_invoice);
frm.toggle_reqd("delivery_note_series", frm.doc.sync_delivery_note);
}
})
$.extend(erpnext_integrations.shopify_settings, {
setup_queries: function(frm) {
frm.fields_dict["warehouse"].get_query = function(doc) {
return {
filters:{
"company": doc.company,
"is_group": "No"
}
}
}
frm.fields_dict["taxes"].grid.get_field("tax_account").get_query = function(doc){
return {
"query": "erpnext.controllers.queries.tax_account_query",
"filters": {
"account_type": ["Tax", "Chargeable", "Expense Account"],
"company": doc.company
}
}
}
frm.fields_dict["cash_bank_account"].get_query = function(doc) {
return {
filters: [
["Account", "account_type", "in", ["Cash", "Bank"]],
["Account", "root_type", "=", "Asset"],
["Account", "is_group", "=",0],
["Account", "company", "=", doc.company]
]
}
}
frm.fields_dict["cost_center"].get_query = function(doc) {
return {
filters:{
"company": doc.company,
"is_group": "No"
}
}
}
frm.fields_dict["price_list"].get_query = function() {
return {
filters:{
"selling": 1
}
}
}
}
})

View File

@ -1,353 +0,0 @@
{
"actions": [],
"creation": "2015-05-18 05:21:07.270859",
"doctype": "DocType",
"document_type": "System",
"engine": "InnoDB",
"field_order": [
"status_html",
"enable_shopify",
"app_type",
"column_break_4",
"last_sync_datetime",
"section_break_2",
"shopify_url",
"api_key",
"column_break_3",
"password",
"shared_secret",
"access_token",
"section_break_38",
"webhooks",
"section_break_15",
"default_customer",
"column_break_19",
"customer_group",
"company_dependent_settings",
"company",
"cash_bank_account",
"column_break_20",
"cost_center",
"erp_settings",
"price_list",
"update_price_in_erpnext_price_list",
"column_break_26",
"warehouse",
"section_break_25",
"sales_order_series",
"column_break_27",
"sync_delivery_note",
"delivery_note_series",
"sync_sales_invoice",
"sales_invoice_series",
"section_break_22",
"html_16",
"taxes",
"syncing_details_section",
"sync_missing_orders",
"sync_based_on",
"column_break_41",
"from_date",
"to_date",
"from_order_id",
"to_order_id",
"last_order_id"
],
"fields": [
{
"fieldname": "status_html",
"fieldtype": "HTML",
"label": "status html",
"read_only": 1
},
{
"default": "0",
"fieldname": "enable_shopify",
"fieldtype": "Check",
"label": "Enable Shopify"
},
{
"default": "Private",
"fieldname": "app_type",
"fieldtype": "Data",
"in_list_view": 1,
"label": "App Type",
"read_only": 1,
"reqd": 1
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"fieldname": "last_sync_datetime",
"fieldtype": "Datetime",
"label": "Last Sync Datetime",
"read_only": 1
},
{
"fieldname": "section_break_2",
"fieldtype": "Section Break"
},
{
"description": "eg: frappe.myshopify.com",
"fieldname": "shopify_url",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Shop URL",
"reqd": 1
},
{
"depends_on": "eval:doc.app_type==\"Private\"",
"fieldname": "api_key",
"fieldtype": "Data",
"label": "API Key"
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"depends_on": "eval:doc.app_type==\"Private\"",
"fieldname": "password",
"fieldtype": "Password",
"label": "Password"
},
{
"fieldname": "shared_secret",
"fieldtype": "Data",
"label": "Shared secret"
},
{
"fieldname": "access_token",
"fieldtype": "Data",
"hidden": 1,
"label": "Access Token",
"read_only": 1
},
{
"collapsible": 1,
"fieldname": "section_break_38",
"fieldtype": "Section Break",
"label": "Webhooks Details"
},
{
"fieldname": "webhooks",
"fieldtype": "Table",
"label": "Webhooks",
"options": "Shopify Webhook Detail",
"read_only": 1
},
{
"fieldname": "section_break_15",
"fieldtype": "Section Break",
"label": "Customer Settings"
},
{
"description": "If Shopify does not have a customer in the order, then while syncing the orders, the system will consider the default customer for the order",
"fieldname": "default_customer",
"fieldtype": "Link",
"label": "Default Customer",
"options": "Customer"
},
{
"fieldname": "column_break_19",
"fieldtype": "Column Break"
},
{
"description": "Customer Group will set to selected group while syncing customers from Shopify",
"fieldname": "customer_group",
"fieldtype": "Link",
"label": "Customer Group",
"options": "Customer Group"
},
{
"fieldname": "company_dependent_settings",
"fieldtype": "Section Break"
},
{
"fieldname": "company",
"fieldtype": "Link",
"label": "For Company",
"options": "Company"
},
{
"description": "Cash Account will used for Sales Invoice creation",
"fieldname": "cash_bank_account",
"fieldtype": "Link",
"label": "Cash/Bank Account",
"options": "Account"
},
{
"fieldname": "column_break_20",
"fieldtype": "Column Break"
},
{
"fieldname": "cost_center",
"fieldtype": "Link",
"label": "Cost Center",
"options": "Cost Center"
},
{
"fieldname": "erp_settings",
"fieldtype": "Section Break"
},
{
"fieldname": "price_list",
"fieldtype": "Link",
"label": "Price List",
"options": "Price List"
},
{
"default": "0",
"fieldname": "update_price_in_erpnext_price_list",
"fieldtype": "Check",
"label": "Update Price from Shopify To ERPNext Price List"
},
{
"fieldname": "column_break_26",
"fieldtype": "Column Break"
},
{
"description": "Default Warehouse to to create Sales Order and Delivery Note",
"fieldname": "warehouse",
"fieldtype": "Link",
"label": "Warehouse",
"options": "Warehouse"
},
{
"fieldname": "section_break_25",
"fieldtype": "Section Break"
},
{
"fieldname": "sales_order_series",
"fieldtype": "Select",
"label": "Sales Order Series"
},
{
"fieldname": "column_break_27",
"fieldtype": "Column Break"
},
{
"default": "0",
"fieldname": "sync_delivery_note",
"fieldtype": "Check",
"label": "Import Delivery Notes from Shopify on Shipment"
},
{
"depends_on": "eval:doc.sync_delivery_note==1",
"fieldname": "delivery_note_series",
"fieldtype": "Select",
"label": "Delivery Note Series"
},
{
"default": "0",
"fieldname": "sync_sales_invoice",
"fieldtype": "Check",
"label": "Import Sales Invoice from Shopify if Payment is marked"
},
{
"depends_on": "eval:doc.sync_sales_invoice==1",
"fieldname": "sales_invoice_series",
"fieldtype": "Select",
"label": "Sales Invoice Series"
},
{
"fieldname": "section_break_22",
"fieldtype": "Section Break"
},
{
"fieldname": "html_16",
"fieldtype": "HTML",
"options": "Map Shopify Taxes / Shipping Charges to ERPNext Account"
},
{
"fieldname": "taxes",
"fieldtype": "Table",
"label": "Shopify Tax Account",
"options": "Shopify Tax Account"
},
{
"collapsible": 1,
"fieldname": "syncing_details_section",
"fieldtype": "Section Break",
"label": "Syncing Missing Orders"
},
{
"depends_on": "eval:doc.sync_missing_orders",
"fieldname": "last_order_id",
"fieldtype": "Data",
"label": "Last Order Id",
"read_only": 1
},
{
"fieldname": "column_break_41",
"fieldtype": "Column Break"
},
{
"default": "0",
"description": "On checking this Order from the ",
"fieldname": "sync_missing_orders",
"fieldtype": "Check",
"label": "Sync Missing Old Shopify Orders"
},
{
"depends_on": "eval:doc.sync_missing_orders",
"fieldname": "sync_based_on",
"fieldtype": "Select",
"label": "Sync Based On",
"mandatory_depends_on": "eval:doc.sync_missing_orders",
"options": "\nDate\nShopify Order Id"
},
{
"depends_on": "eval:doc.sync_based_on == 'Date' && doc.sync_missing_orders",
"fieldname": "from_date",
"fieldtype": "Date",
"label": "From Date",
"mandatory_depends_on": "eval:doc.sync_based_on == 'Date' && doc.sync_missing_orders"
},
{
"depends_on": "eval:doc.sync_based_on == 'Date' && doc.sync_missing_orders",
"fieldname": "to_date",
"fieldtype": "Date",
"label": "To Date",
"mandatory_depends_on": "eval:doc.sync_based_on == 'Date' && doc.sync_missing_orders"
},
{
"depends_on": "eval:doc.sync_based_on == 'Shopify Order Id' && doc.sync_missing_orders",
"fieldname": "from_order_id",
"fieldtype": "Data",
"label": "From Order Id",
"mandatory_depends_on": "eval:doc.sync_based_on == 'Shopify Order Id' && doc.sync_missing_orders"
},
{
"depends_on": "eval:doc.sync_based_on == 'Shopify Order Id' && doc.sync_missing_orders",
"fieldname": "to_order_id",
"fieldtype": "Data",
"label": "To Order Id",
"mandatory_depends_on": "eval:doc.sync_based_on == 'Shopify Order Id' && doc.sync_missing_orders"
}
],
"issingle": 1,
"links": [],
"modified": "2021-03-02 17:35:41.953317",
"modified_by": "Administrator",
"module": "ERPNext Integrations",
"name": "Shopify Settings",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -1,144 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
import json
from frappe import _
from frappe.model.document import Document
from frappe.utils import get_request_session
from requests.exceptions import HTTPError
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
from erpnext.erpnext_integrations.utils import get_webhook_address
from erpnext.erpnext_integrations.doctype.shopify_log.shopify_log import make_shopify_log
class ShopifySettings(Document):
def validate(self):
if self.enable_shopify == 1:
setup_custom_fields()
self.validate_access_credentials()
self.register_webhooks()
else:
self.unregister_webhooks()
def validate_access_credentials(self):
if not (self.get_password(raise_exception=False) and self.api_key and self.shopify_url):
frappe.msgprint(_("Missing value for Password, API Key or Shopify URL"), raise_exception=frappe.ValidationError)
def register_webhooks(self):
webhooks = ["orders/create", "orders/paid", "orders/fulfilled"]
# url = get_shopify_url('admin/webhooks.json', self)
created_webhooks = [d.method for d in self.webhooks]
url = get_shopify_url('admin/api/2021-04/webhooks.json', self)
for method in webhooks:
session = get_request_session()
try:
res = session.post(url, data=json.dumps({
"webhook": {
"topic": method,
"address": get_webhook_address(connector_name='shopify_connection', method='store_request_data', force_https=True),
"format": "json"
}
}), headers=get_header(self))
res.raise_for_status()
self.update_webhook_table(method, res.json())
except HTTPError as e:
error_message = res.json().get('errors', e)
make_shopify_log(status="Warning", exception=error_message, rollback=True)
except Exception as e:
make_shopify_log(status="Warning", exception=e, rollback=True)
def unregister_webhooks(self):
session = get_request_session()
deleted_webhooks = []
for d in self.webhooks:
url = get_shopify_url('admin/api/2021-04/webhooks/{0}.json'.format(d.webhook_id), self)
try:
res = session.delete(url, headers=get_header(self))
res.raise_for_status()
deleted_webhooks.append(d)
except HTTPError as e:
error_message = res.json().get('errors', e)
make_shopify_log(status="Warning", exception=error_message, rollback=True)
except Exception as e:
frappe.log_error(message=e, title='Shopify Webhooks Issue')
for d in deleted_webhooks:
self.remove(d)
def update_webhook_table(self, method, res):
self.append("webhooks", {
"webhook_id": res['webhook']['id'],
"method": method
})
def get_shopify_url(path, settings):
if settings.app_type == "Private":
return 'https://{}:{}@{}/{}'.format(settings.api_key, settings.get_password('password'), settings.shopify_url, path)
else:
return 'https://{}/{}'.format(settings.shopify_url, path)
def get_header(settings):
header = {'Content-Type': 'application/json'}
return header
@frappe.whitelist()
def get_series():
return {
"sales_order_series" : frappe.get_meta("Sales Order").get_options("naming_series") or "SO-Shopify-",
"sales_invoice_series" : frappe.get_meta("Sales Invoice").get_options("naming_series") or "SI-Shopify-",
"delivery_note_series" : frappe.get_meta("Delivery Note").get_options("naming_series") or "DN-Shopify-"
}
def setup_custom_fields():
custom_fields = {
"Customer": [
dict(fieldname='shopify_customer_id', label='Shopify Customer Id',
fieldtype='Data', insert_after='series', read_only=1, print_hide=1)
],
"Supplier": [
dict(fieldname='shopify_supplier_id', label='Shopify Supplier Id',
fieldtype='Data', insert_after='supplier_name', read_only=1, print_hide=1)
],
"Address": [
dict(fieldname='shopify_address_id', label='Shopify Address Id',
fieldtype='Data', insert_after='fax', read_only=1, print_hide=1)
],
"Item": [
dict(fieldname='shopify_variant_id', label='Shopify Variant Id',
fieldtype='Data', insert_after='item_code', read_only=1, print_hide=1),
dict(fieldname='shopify_product_id', label='Shopify Product Id',
fieldtype='Data', insert_after='item_code', read_only=1, print_hide=1),
dict(fieldname='shopify_description', label='Shopify Description',
fieldtype='Text Editor', insert_after='description', read_only=1, print_hide=1)
],
"Sales Order": [
dict(fieldname='shopify_order_id', label='Shopify Order Id',
fieldtype='Data', insert_after='title', read_only=1, print_hide=1),
dict(fieldname='shopify_order_number', label='Shopify Order Number',
fieldtype='Data', insert_after='shopify_order_id', read_only=1, print_hide=1)
],
"Delivery Note":[
dict(fieldname='shopify_order_id', label='Shopify Order Id',
fieldtype='Data', insert_after='title', read_only=1, print_hide=1),
dict(fieldname='shopify_order_number', label='Shopify Order Number',
fieldtype='Data', insert_after='shopify_order_id', read_only=1, print_hide=1),
dict(fieldname='shopify_fulfillment_id', label='Shopify Fulfillment Id',
fieldtype='Data', insert_after='title', read_only=1, print_hide=1)
],
"Sales Invoice": [
dict(fieldname='shopify_order_id', label='Shopify Order Id',
fieldtype='Data', insert_after='title', read_only=1, print_hide=1),
dict(fieldname='shopify_order_number', label='Shopify Order Number',
fieldtype='Data', insert_after='shopify_order_id', read_only=1, print_hide=1)
]
}
create_custom_fields(custom_fields)

View File

@ -1,71 +0,0 @@
from __future__ import unicode_literals
import frappe
from frappe import _
def create_customer(shopify_customer, shopify_settings):
import frappe.utils.nestedset
cust_name = (shopify_customer.get("first_name") + " " + (shopify_customer.get("last_name") \
and shopify_customer.get("last_name") or "")) if shopify_customer.get("first_name")\
else shopify_customer.get("email")
try:
customer = frappe.get_doc({
"doctype": "Customer",
"name": shopify_customer.get("id"),
"customer_name" : cust_name,
"shopify_customer_id": shopify_customer.get("id"),
"sync_with_shopify": 1,
"customer_group": shopify_settings.customer_group,
"territory": frappe.utils.nestedset.get_root_of("Territory"),
"customer_type": _("Individual")
})
customer.flags.ignore_mandatory = True
customer.insert(ignore_permissions=True)
if customer:
create_customer_address(customer, shopify_customer)
frappe.db.commit()
except Exception as e:
raise e
def create_customer_address(customer, shopify_customer):
addresses = shopify_customer.get("addresses", [])
if not addresses and "default_address" in shopify_customer:
addresses.append(shopify_customer["default_address"])
for i, address in enumerate(addresses):
address_title, address_type = get_address_title_and_type(customer.customer_name, i)
try :
frappe.get_doc({
"doctype": "Address",
"shopify_address_id": address.get("id"),
"address_title": address_title,
"address_type": address_type,
"address_line1": address.get("address1") or "Address 1",
"address_line2": address.get("address2"),
"city": address.get("city") or "City",
"state": address.get("province"),
"pincode": address.get("zip"),
"country": address.get("country"),
"phone": address.get("phone"),
"email_id": shopify_customer.get("email"),
"links": [{
"link_doctype": "Customer",
"link_name": customer.name
}]
}).insert(ignore_mandatory=True)
except Exception as e:
raise e
def get_address_title_and_type(customer_name, index):
address_type = _("Billing")
address_title = customer_name
if frappe.db.get_value("Address", "{0}-{1}".format(customer_name.strip(), address_type)):
address_title = "{0}-{1}".format(customer_name.strip(), index)
return address_title, address_type

View File

@ -1,309 +0,0 @@
from __future__ import unicode_literals
import frappe
from frappe import _
from erpnext import get_default_company
from frappe.utils import cstr, cint, get_request_session
from erpnext.erpnext_integrations.doctype.shopify_settings.shopify_settings import get_shopify_url, get_header
shopify_variants_attr_list = ["option1", "option2", "option3"]
def sync_item_from_shopify(shopify_settings, item):
url = get_shopify_url("admin/api/2021-04/products/{0}.json".format(item.get("product_id")), shopify_settings)
session = get_request_session()
try:
res = session.get(url, headers=get_header(shopify_settings))
res.raise_for_status()
shopify_item = res.json()["product"]
make_item(shopify_settings.warehouse, shopify_item)
except Exception as e:
raise e
def make_item(warehouse, shopify_item):
add_item_weight(shopify_item)
if has_variants(shopify_item):
attributes = create_attribute(shopify_item)
create_item(shopify_item, warehouse, 1, attributes)
create_item_variants(shopify_item, warehouse, attributes, shopify_variants_attr_list)
else:
shopify_item["variant_id"] = shopify_item['variants'][0]["id"]
create_item(shopify_item, warehouse)
def add_item_weight(shopify_item):
shopify_item["weight"] = shopify_item['variants'][0]["weight"]
shopify_item["weight_unit"] = shopify_item['variants'][0]["weight_unit"]
def has_variants(shopify_item):
if len(shopify_item.get("options")) >= 1 and "Default Title" not in shopify_item.get("options")[0]["values"]:
return True
return False
def create_attribute(shopify_item):
attribute = []
# shopify item dict
for attr in shopify_item.get('options'):
if not frappe.db.get_value("Item Attribute", attr.get("name"), "name"):
frappe.get_doc({
"doctype": "Item Attribute",
"attribute_name": attr.get("name"),
"item_attribute_values": [
{
"attribute_value": attr_value,
"abbr":attr_value
}
for attr_value in attr.get("values")
]
}).insert()
attribute.append({"attribute": attr.get("name")})
else:
# check for attribute values
item_attr = frappe.get_doc("Item Attribute", attr.get("name"))
if not item_attr.numeric_values:
set_new_attribute_values(item_attr, attr.get("values"))
item_attr.save()
attribute.append({"attribute": attr.get("name")})
else:
attribute.append({
"attribute": attr.get("name"),
"from_range": item_attr.get("from_range"),
"to_range": item_attr.get("to_range"),
"increment": item_attr.get("increment"),
"numeric_values": item_attr.get("numeric_values")
})
return attribute
def set_new_attribute_values(item_attr, values):
for attr_value in values:
if not any((d.abbr.lower() == attr_value.lower() or d.attribute_value.lower() == attr_value.lower())\
for d in item_attr.item_attribute_values):
item_attr.append("item_attribute_values", {
"attribute_value": attr_value,
"abbr": attr_value
})
def create_item(shopify_item, warehouse, has_variant=0, attributes=None,variant_of=None):
item_dict = {
"doctype": "Item",
"shopify_product_id": shopify_item.get("id"),
"shopify_variant_id": shopify_item.get("variant_id"),
"variant_of": variant_of,
"sync_with_shopify": 1,
"is_stock_item": 1,
"item_code": cstr(shopify_item.get("item_code")) or cstr(shopify_item.get("id")),
"item_name": shopify_item.get("title", '').strip(),
"description": shopify_item.get("body_html") or shopify_item.get("title"),
"shopify_description": shopify_item.get("body_html") or shopify_item.get("title"),
"item_group": get_item_group(shopify_item.get("product_type")),
"has_variants": has_variant,
"attributes":attributes or [],
"stock_uom": shopify_item.get("uom") or _("Nos"),
"stock_keeping_unit": shopify_item.get("sku") or get_sku(shopify_item),
"default_warehouse": warehouse,
"image": get_item_image(shopify_item),
"weight_uom": shopify_item.get("weight_unit"),
"weight_per_unit": shopify_item.get("weight"),
"default_supplier": get_supplier(shopify_item),
"item_defaults": [
{
"company": get_default_company()
}
]
}
if not is_item_exists(item_dict, attributes, variant_of=variant_of):
item_details = get_item_details(shopify_item)
name = ''
if not item_details:
new_item = frappe.get_doc(item_dict)
new_item.insert(ignore_permissions=True, ignore_mandatory=True)
name = new_item.name
if not name:
name = item_details.name
if not has_variant:
add_to_price_list(shopify_item, name)
frappe.db.commit()
def create_item_variants(shopify_item, warehouse, attributes, shopify_variants_attr_list):
template_item = frappe.db.get_value("Item", filters={"shopify_product_id": shopify_item.get("id")},
fieldname=["name", "stock_uom"], as_dict=True)
if template_item:
for variant in shopify_item.get("variants"):
shopify_item_variant = {
"id" : variant.get("id"),
"item_code": variant.get("id"),
"title": variant.get("title"),
"product_type": shopify_item.get("product_type"),
"sku": variant.get("sku"),
"uom": template_item.stock_uom or _("Nos"),
"item_price": variant.get("price"),
"variant_id": variant.get("id"),
"weight_unit": variant.get("weight_unit"),
"weight": variant.get("weight")
}
for i, variant_attr in enumerate(shopify_variants_attr_list):
if variant.get(variant_attr):
attributes[i].update({"attribute_value": get_attribute_value(variant.get(variant_attr), attributes[i])})
create_item(shopify_item_variant, warehouse, 0, attributes, template_item.name)
def get_attribute_value(variant_attr_val, attribute):
attribute_value = frappe.db.sql("""select attribute_value from `tabItem Attribute Value`
where parent = %s and (abbr = %s or attribute_value = %s)""", (attribute["attribute"], variant_attr_val,
variant_attr_val), as_list=1)
return attribute_value[0][0] if len(attribute_value)>0 else cint(variant_attr_val)
def get_item_group(product_type=None):
import frappe.utils.nestedset
parent_item_group = frappe.utils.nestedset.get_root_of("Item Group")
if product_type:
if not frappe.db.get_value("Item Group", product_type, "name"):
item_group = frappe.get_doc({
"doctype": "Item Group",
"item_group_name": product_type,
"parent_item_group": parent_item_group,
"is_group": "No"
}).insert()
return item_group.name
else:
return product_type
else:
return parent_item_group
def get_sku(item):
if item.get("variants"):
return item.get("variants")[0].get("sku")
return ""
def add_to_price_list(item, name):
shopify_settings = frappe.db.get_value("Shopify Settings", None, ["price_list", "update_price_in_erpnext_price_list"], as_dict=1)
if not shopify_settings.update_price_in_erpnext_price_list:
return
item_price_name = frappe.db.get_value("Item Price",
{"item_code": name, "price_list": shopify_settings.price_list}, "name")
if not item_price_name:
frappe.get_doc({
"doctype": "Item Price",
"price_list": shopify_settings.price_list,
"item_code": name,
"price_list_rate": item.get("item_price") or item.get("variants")[0].get("price")
}).insert()
else:
item_rate = frappe.get_doc("Item Price", item_price_name)
item_rate.price_list_rate = item.get("item_price") or item.get("variants")[0].get("price")
item_rate.save()
def get_item_image(shopify_item):
if shopify_item.get("image"):
return shopify_item.get("image").get("src")
return None
def get_supplier(shopify_item):
if shopify_item.get("vendor"):
supplier = frappe.db.sql("""select name from tabSupplier
where name = %s or shopify_supplier_id = %s """, (shopify_item.get("vendor"),
shopify_item.get("vendor").lower()), as_list=1)
if not supplier:
supplier = frappe.get_doc({
"doctype": "Supplier",
"supplier_name": shopify_item.get("vendor"),
"shopify_supplier_id": shopify_item.get("vendor").lower(),
"supplier_group": get_supplier_group()
}).insert()
return supplier.name
else:
return shopify_item.get("vendor")
else:
return ""
def get_supplier_group():
supplier_group = frappe.db.get_value("Supplier Group", _("Shopify Supplier"))
if not supplier_group:
supplier_group = frappe.get_doc({
"doctype": "Supplier Group",
"supplier_group_name": _("Shopify Supplier")
}).insert()
return supplier_group.name
return supplier_group
def get_item_details(shopify_item):
item_details = {}
item_details = frappe.db.get_value("Item", {"shopify_product_id": shopify_item.get("id")},
["name", "stock_uom", "item_name"], as_dict=1)
if item_details:
return item_details
else:
item_details = frappe.db.get_value("Item", {"shopify_variant_id": shopify_item.get("id")},
["name", "stock_uom", "item_name"], as_dict=1)
return item_details
def is_item_exists(shopify_item, attributes=None, variant_of=None):
if variant_of:
name = variant_of
else:
name = frappe.db.get_value("Item", {"item_name": shopify_item.get("item_name")})
if name:
item = frappe.get_doc("Item", name)
item.flags.ignore_mandatory=True
if not variant_of and not item.shopify_product_id:
item.shopify_product_id = shopify_item.get("shopify_product_id")
item.shopify_variant_id = shopify_item.get("shopify_variant_id")
item.save()
return True
if item.shopify_product_id and attributes and attributes[0].get("attribute_value"):
if not variant_of:
variant_of = frappe.db.get_value("Item",
{"shopify_product_id": item.shopify_product_id}, "variant_of")
# create conditions for all item attributes,
# as we are putting condition basis on OR it will fetch all items matching either of conditions
# thus comparing matching conditions with len(attributes)
# which will give exact matching variant item.
conditions = ["(iv.attribute='{0}' and iv.attribute_value = '{1}')"\
.format(attr.get("attribute"), attr.get("attribute_value")) for attr in attributes]
conditions = "( {0} ) and iv.parent = it.name ) = {1}".format(" or ".join(conditions), len(attributes))
parent = frappe.db.sql(""" select * from tabItem it where
( select count(*) from `tabItem Variant Attribute` iv
where {conditions} and it.variant_of = %s """.format(conditions=conditions) ,
variant_of, as_list=1)
if parent:
variant = frappe.get_doc("Item", parent[0][0])
variant.flags.ignore_mandatory = True
variant.shopify_product_id = shopify_item.get("shopify_product_id")
variant.shopify_variant_id = shopify_item.get("shopify_variant_id")
variant.save()
return False
if item.shopify_product_id and item.shopify_product_id != shopify_item.get("shopify_product_id"):
return False
return True
else:
return False

View File

@ -1,527 +0,0 @@
[
{
"allow_on_submit": 0,
"collapsible": 0,
"collapsible_depends_on": null,
"default": null,
"depends_on": null,
"description": null,
"docstatus": 0,
"doctype": "Custom Field",
"dt": "Print Settings",
"fieldname": "compact_item_print",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"insert_after": "with_letterhead",
"label": "Compact Item Print",
"modified": "2016-06-06 15:18:17.025602",
"name": "Print Settings-compact_item_print",
"no_copy": 0,
"options": null,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": null,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"unique": 0,
"width": null
},
{
"allow_on_submit": 0,
"collapsible": 0,
"collapsible_depends_on": null,
"default": null,
"depends_on": null,
"description": null,
"docstatus": 0,
"doctype": "Custom Field",
"dt": "Customer",
"fieldname": "shopify_customer_id",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"insert_after": "naming_series",
"label": "Shopify Customer Id",
"modified": "2016-01-15 17:25:28.991818",
"name": "Customer-shopify_customer_id",
"no_copy": 1,
"options": null,
"permlevel": 0,
"precision": "",
"print_hide": 1,
"print_hide_if_no_value": 0,
"print_width": null,
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"unique": 0,
"width": null
},
{
"allow_on_submit": 0,
"collapsible": 0,
"collapsible_depends_on": null,
"default": null,
"depends_on": null,
"description": null,
"docstatus": 0,
"doctype": "Custom Field",
"dt": "Address",
"fieldname": "shopify_address_id",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"insert_after": "fax",
"label": "Shopify Address Id",
"modified": "2016-01-15 17:50:52.213743",
"name": "Address-shopify_address_id",
"no_copy": 1,
"options": null,
"permlevel": 0,
"precision": "",
"print_hide": 1,
"print_hide_if_no_value": 0,
"print_width": null,
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"unique": 0,
"width": null
},
{
"allow_on_submit": 0,
"collapsible": 0,
"collapsible_depends_on": null,
"default": null,
"depends_on": null,
"description": null,
"docstatus": 0,
"doctype": "Custom Field",
"dt": "Sales Order",
"fieldname": "shopify_order_id",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"insert_after": "title",
"label": "Shopify Order Id",
"modified": "2016-01-18 09:55:50.764524",
"name": "Sales Order-shopify_order_id",
"no_copy": 1,
"options": null,
"permlevel": 0,
"precision": "",
"print_hide": 1,
"print_hide_if_no_value": 0,
"print_width": null,
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"unique": 0,
"width": null
},
{
"allow_on_submit": 0,
"collapsible": 0,
"collapsible_depends_on": null,
"default": null,
"depends_on": null,
"description": null,
"docstatus": 0,
"doctype": "Custom Field",
"dt": "Item",
"fieldname": "shopify_product_id",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"insert_after": "item_code",
"label": "Shopify Product Id",
"modified": "2016-01-19 15:44:16.132952",
"name": "Item-shopify_product_id",
"no_copy": 1,
"options": null,
"permlevel": 0,
"precision": "",
"print_hide": 1,
"print_hide_if_no_value": 0,
"print_width": null,
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"unique": 0,
"width": null
},
{
"allow_on_submit": 0,
"collapsible": 0,
"collapsible_depends_on": null,
"default": null,
"depends_on": null,
"description": null,
"docstatus": 0,
"doctype": "Custom Field",
"dt": "Sales Invoice",
"fieldname": "shopify_order_id",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"insert_after": "naming_series",
"label": "Shopify Order Id",
"modified": "2016-01-19 16:30:12.261797",
"name": "Sales Invoice-shopify_order_id",
"no_copy": 1,
"options": null,
"permlevel": 0,
"precision": "",
"print_hide": 1,
"print_hide_if_no_value": 0,
"print_width": null,
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"unique": 0,
"width": null
},
{
"allow_on_submit": 0,
"collapsible": 0,
"collapsible_depends_on": null,
"default": null,
"depends_on": null,
"description": null,
"docstatus": 0,
"doctype": "Custom Field",
"dt": "Delivery Note",
"fieldname": "shopify_order_id",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"insert_after": "title",
"label": "Shopify Order Id",
"modified": "2016-01-19 16:30:31.201198",
"name": "Delivery Note-shopify_order_id",
"no_copy": 1,
"options": null,
"permlevel": 0,
"precision": "",
"print_hide": 1,
"print_hide_if_no_value": 0,
"print_width": null,
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"unique": 0,
"width": null
},
{
"allow_on_submit": 0,
"collapsible": 0,
"collapsible_depends_on": null,
"default": null,
"depends_on": null,
"description": null,
"docstatus": 0,
"doctype": "Custom Field",
"dt": "Item",
"fieldname": "stock_keeping_unit",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"insert_after": "stock_uom",
"label": "Stock Keeping Unit",
"modified": "2015-11-10 09:29:10.854943",
"name": "Item-stock_keeping_unit",
"no_copy": 1,
"options": null,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": null,
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"unique": 0,
"width": null
},
{
"allow_on_submit": 0,
"collapsible": 0,
"collapsible_depends_on": null,
"default": "0",
"depends_on": null,
"description": null,
"docstatus": 0,
"doctype": "Custom Field",
"dt": "Item",
"fieldname": "sync_with_shopify",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"insert_after": "is_stock_item",
"label": "Sync With Shopify",
"modified": "2015-10-12 15:54:31.997714",
"name": "Item-sync_with_shopify",
"no_copy": 0,
"options": null,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": null,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"unique": 0,
"width": null
},
{
"allow_on_submit": 0,
"collapsible": 0,
"collapsible_depends_on": null,
"default": null,
"depends_on": null,
"description": null,
"docstatus": 0,
"doctype": "Custom Field",
"dt": "Customer",
"fieldname": "sync_with_shopify",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"insert_after": "is_frozen",
"label": "Sync With Shopify",
"modified": "2015-10-01 17:31:55.758826",
"name": "Customer-sync_with_shopify",
"no_copy": 0,
"options": null,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": null,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"unique": 0,
"width": null
},
{
"allow_on_submit": 0,
"collapsible": 0,
"collapsible_depends_on": null,
"default": null,
"depends_on": null,
"description": null,
"docstatus": 0,
"doctype": "Custom Field",
"dt": "Item",
"fieldname": "shopify_variant_id",
"fieldtype": "Data",
"hidden": 1,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"insert_after": "item_code",
"label": "Variant Id",
"modified": "2015-11-09 18:26:50.825858",
"name": "Item-shopify_variant_id",
"no_copy": 1,
"options": null,
"permlevel": 0,
"precision": "",
"print_hide": 1,
"print_hide_if_no_value": 0,
"print_width": null,
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"unique": 0,
"width": null
},
{
"allow_on_submit": 0,
"collapsible": 0,
"collapsible_depends_on": null,
"default": null,
"depends_on": null,
"description": null,
"docstatus": 0,
"doctype": "Custom Field",
"dt": "Item",
"fieldname": "sync_qty_with_shopify",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"insert_after": "item_code",
"label": "Sync Quantity With Shopify",
"modified": "2015-12-29 08:37:46.183295",
"name": "Item-sync_qty_with_shopify",
"no_copy": 0,
"options": null,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": null,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"unique": 0,
"width": null
},
{
"allow_on_submit": 0,
"collapsible": 0,
"collapsible_depends_on": null,
"default": null,
"depends_on": null,
"description": null,
"docstatus": 0,
"doctype": "Custom Field",
"dt": "Delivery Note",
"fieldname": "shopify_fulfillment_id",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"insert_after": "title",
"label": "Shopify Fulfillment Id",
"modified": "2016-01-20 23:50:35.609543",
"name": "Delivery Note-shopify_fulfillment_id",
"no_copy": 1,
"options": null,
"permlevel": 0,
"precision": "",
"print_hide": 1,
"print_hide_if_no_value": 0,
"print_width": null,
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"unique": 0,
"width": null
},
{
"allow_on_submit": 0,
"collapsible": 0,
"collapsible_depends_on": null,
"default": null,
"depends_on": null,
"description": null,
"docstatus": 0,
"doctype": "Custom Field",
"dt": "Supplier",
"fieldname": "shopify_supplier_id",
"fieldtype": "Data",
"hidden": 1,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"insert_after": "supplier_name",
"label": "Shopify Supplier Id",
"modified": "2016-02-01 15:41:25.818306",
"name": "Supplier-shopify_supplier_id",
"no_copy": 1,
"options": null,
"permlevel": 0,
"precision": "",
"print_hide": 1,
"print_hide_if_no_value": 0,
"print_width": null,
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"unique": 0,
"width": null
},
{
"allow_on_submit": 0,
"collapsible": 0,
"collapsible_depends_on": null,
"default": null,
"depends_on": null,
"description": null,
"docstatus": 0,
"doctype": "Custom Field",
"dt": "Item",
"fieldname": "shopify_description",
"fieldtype": "Text Editor",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"insert_after": "section_break_11",
"label": "shopify_description",
"modified": "2016-06-15 12:15:36.325581",
"name": "Item-shopify_description",
"no_copy": 0,
"options": null,
"permlevel": 0,
"precision": "",
"print_hide": 1,
"print_hide_if_no_value": 0,
"print_width": null,
"read_only": 0,
"report_hide": 1,
"reqd": 0,
"search_index": 0,
"unique": 0,
"width": null
}
]

View File

@ -1,59 +0,0 @@
{
"customer": {
"id": 2324518599,
"email": "andrew@wyatt.co.in",
"accepts_marketing": false,
"created_at": "2016-01-20T17:18:35+05:30",
"updated_at": "2016-01-20T17:22:23+05:30",
"first_name": "Andrew",
"last_name": "Wyatt",
"orders_count": 0,
"state": "disabled",
"total_spent": "0.00",
"last_order_id": null,
"note": "",
"verified_email": true,
"multipass_identifier": null,
"tax_exempt": false,
"tags": "",
"last_order_name": null,
"default_address": {
"id": 2476804295,
"first_name": "Andrew",
"last_name": "Wyatt",
"company": "Wyatt Inc.",
"address1": "B-11, Betahouse",
"address2": "Street 11, Sector 52",
"city": "Manhattan",
"province": "New York",
"country": "United States",
"zip": "10027",
"phone": "145-112211",
"name": "Andrew Wyatt",
"province_code": "NY",
"country_code": "US",
"country_name": "United States",
"default": true
},
"addresses": [
{
"id": 2476804295,
"first_name": "Andrew",
"last_name": "Wyatt",
"company": "Wyatt Inc.",
"address1": "B-11, Betahouse",
"address2": "Street 11, Sector 52",
"city": "Manhattan",
"province": "New York",
"country": "United States",
"zip": "10027",
"phone": "145-112211",
"name": "Andrew Wyatt",
"province_code": "NY",
"country_code": "US",
"country_name": "United States",
"default": true
}
]
}
}

View File

@ -1,125 +0,0 @@
{
"product": {
"id": 4059739520,
"title": "Shopify Test Item",
"body_html": "<div>Hold back Spin Medallion-Set of 2</div>\n<div></div>\n<div>Finish: Plated/ Powder Coated</div>\n<div>Material: Iron</div>\n<div>Color Finish: Satin Silver, Brown Oil Rubbed, Roman Bronze</div>\n<div>Qty: 1 Set</div>",
"vendor": "Boa casa",
"product_type": "Curtain Accessories",
"created_at": "2016-01-18T17:16:37+05:30",
"handle": "1001624-01",
"updated_at": "2016-01-20T17:26:44+05:30",
"published_at": "2016-01-18T17:16:37+05:30",
"template_suffix": null,
"published_scope": "global",
"tags": "Category_Curtain Accessories, Type_Holdback",
"variants": [{
"id": 13917612359,
"product_id": 4059739520,
"title": "Test BALCK Item",
"price": "499.00",
"sku": "",
"position": 1,
"grams": 0,
"inventory_policy": "continue",
"compare_at_price": null,
"fulfillment_service": "manual",
"inventory_management": "shopify",
"option1": "BLACK",
"option2": null,
"option3": null,
"created_at": "2016-01-18T17:16:37+05:30",
"updated_at": "2016-01-20T17:26:44+05:30",
"requires_shipping": true,
"taxable": true,
"barcode": "",
"inventory_quantity": -1,
"old_inventory_quantity": -1,
"image_id": 8539321735,
"weight": 0,
"weight_unit": "kg"
}, {
"id": 13917612423,
"product_id": 4059739520,
"title": "Test BLUE Item",
"price": "499.00",
"sku": "",
"position": 2,
"grams": 0,
"inventory_policy": "continue",
"compare_at_price": null,
"fulfillment_service": "manual",
"inventory_management": "shopify",
"option1": "BLUE",
"option2": null,
"option3": null,
"created_at": "2016-01-18T17:16:37+05:30",
"updated_at": "2016-01-20T17:26:44+05:30",
"requires_shipping": true,
"taxable": true,
"barcode": "",
"inventory_quantity": -1,
"old_inventory_quantity": -1,
"image_id": null,
"weight": 0,
"weight_unit": "kg"
}, {
"id": 13917612487,
"product_id": 4059739520,
"title": "Test White Item",
"price": "499.00",
"sku": "",
"position": 3,
"grams": 0,
"inventory_policy": "continue",
"compare_at_price": null,
"fulfillment_service": "manual",
"inventory_management": "shopify",
"option1": "White",
"option2": null,
"option3": null,
"created_at": "2016-01-18T17:16:37+05:30",
"updated_at": "2016-01-18T17:16:37+05:30",
"requires_shipping": true,
"taxable": true,
"barcode": "",
"inventory_quantity": 0,
"old_inventory_quantity": 0,
"image_id": null,
"weight": 0,
"weight_unit": "kg"
}],
"options": [{
"id": 4985027399,
"product_id": 4059739520,
"name": "Colour",
"position": 1,
"values": [
"BLACK",
"BLUE",
"White"
]
}],
"images": [{
"id": 8539321735,
"product_id": 4059739520,
"position": 1,
"created_at": "2016-01-18T17:16:37+05:30",
"updated_at": "2016-01-18T17:16:37+05:30",
"src": "https://cdn.shopify.com/s/files/1/1123/0654/products/2015-12-17_6.png?v=1453117597",
"variant_ids": [
13917612359
]
}],
"image": {
"id": 8539321735,
"product_id": 4059739520,
"position": 1,
"created_at": "2016-01-18T17:16:37+05:30",
"updated_at": "2016-01-18T17:16:37+05:30",
"src": "https://cdn.shopify.com/s/files/1/1123/0654/products/2015-12-17_6.png?v=1453117597",
"variant_ids": [
13917612359
]
}
}
}

View File

@ -1,270 +0,0 @@
{
"order": {
"id": 2414345735,
"email": "andrew@wyatt.co.in",
"closed_at": null,
"created_at": "2016-01-20T17:26:39+05:30",
"updated_at": "2016-01-20T17:27:15+05:30",
"number": 5,
"note": "",
"token": "660fed25987517b733644a8c9ec7c8e0",
"gateway": "manual",
"test": false,
"total_price": "1018.00",
"subtotal_price": "998.00",
"total_weight": 0,
"total_tax": "0.00",
"taxes_included": false,
"currency": "INR",
"financial_status": "paid",
"confirmed": true,
"total_discounts": "0.00",
"total_line_items_price": "998.00",
"cart_token": null,
"buyer_accepts_marketing": false,
"name": "#1005",
"referring_site": null,
"landing_site": null,
"cancelled_at": null,
"cancel_reason": null,
"total_price_usd": "15.02",
"checkout_token": null,
"reference": null,
"user_id": 55391175,
"location_id": null,
"source_identifier": null,
"source_url": null,
"processed_at": "2016-01-20T17:26:39+05:30",
"device_id": null,
"browser_ip": null,
"landing_site_ref": null,
"order_number": 1005,
"discount_codes": [],
"note_attributes": [],
"payment_gateway_names": [
"manual"
],
"processing_method": "manual",
"checkout_id": null,
"source_name": "shopify_draft_order",
"fulfillment_status": "fulfilled",
"tax_lines": [],
"tags": "",
"contact_email": "andrew@wyatt.co.in",
"line_items": [
{
"id": 4125768135,
"variant_id": 13917612359,
"title": "Shopify Test Item",
"quantity": 1,
"price": "499.00",
"grams": 0,
"sku": "",
"variant_title": "Roman BALCK 1",
"vendor": "Boa casa",
"fulfillment_service": "manual",
"product_id": 4059739527,
"requires_shipping": true,
"taxable": true,
"gift_card": false,
"name": "Roman BALCK 1",
"variant_inventory_management": "shopify",
"properties": [],
"product_exists": true,
"fulfillable_quantity": 0,
"total_discount": "0.00",
"fulfillment_status": "fulfilled",
"tax_lines": []
},
{
"id": 4125768199,
"variant_id": 13917612423,
"title": "Shopify Test Item",
"quantity": 1,
"price": "499.00",
"grams": 0,
"sku": "",
"variant_title": "Satin BLUE 1",
"vendor": "Boa casa",
"fulfillment_service": "manual",
"product_id": 4059739527,
"requires_shipping": true,
"taxable": true,
"gift_card": false,
"name": "Satin BLUE 1",
"variant_inventory_management": "shopify",
"properties": [],
"product_exists": true,
"fulfillable_quantity": 0,
"total_discount": "0.00",
"fulfillment_status": "fulfilled",
"tax_lines": []
}
],
"shipping_lines": [
{
"id": 2108906247,
"title": "International Shipping",
"price": "20.00",
"code": "International Shipping",
"source": "shopify",
"phone": null,
"tax_lines": []
}
],
"billing_address": {
"first_name": "Andrew",
"address1": "B-11, Betahouse",
"phone": "145-112211",
"city": "Manhattan",
"zip": "10027",
"province": "New York",
"country": "United States",
"last_name": "Wyatt",
"address2": "Street 11, Sector 52",
"company": "Wyatt Inc.",
"latitude": 40.8138912,
"longitude": -73.96243270000001,
"name": "Andrew Wyatt",
"country_code": "US",
"province_code": "NY"
},
"shipping_address": {
"first_name": "Andrew",
"address1": "B-11, Betahouse",
"phone": "145-112211",
"city": "Manhattan",
"zip": "10027",
"province": "New York",
"country": "United States",
"last_name": "Wyatt",
"address2": "Street 11, Sector 52",
"company": "Wyatt Inc.",
"latitude": 40.8138912,
"longitude": -73.96243270000001,
"name": "Andrew Wyatt",
"country_code": "US",
"province_code": "NY"
},
"fulfillments": [
{
"id": 1849629255,
"order_id": 2414345735,
"status": "success",
"created_at": "2016-01-20T17:27:15+05:30",
"service": "manual",
"updated_at": "2016-01-20T17:27:15+05:30",
"tracking_company": null,
"tracking_number": null,
"tracking_numbers": [],
"tracking_url": null,
"tracking_urls": [],
"receipt": {},
"line_items": [
{
"id": 4125768199,
"variant_id": 13917612423,
"title": "1001624/01",
"quantity": 1,
"price": "499.00",
"grams": 0,
"sku": "",
"variant_title": "Satin Silver",
"vendor": "Boa casa",
"fulfillment_service": "manual",
"product_id": 4059739527,
"requires_shipping": true,
"taxable": true,
"gift_card": false,
"name": "1001624/01 - Satin Silver",
"variant_inventory_management": "shopify",
"properties": [],
"product_exists": true,
"fulfillable_quantity": 0,
"total_discount": "0.00",
"fulfillment_status": "fulfilled",
"tax_lines": []
}
]
},
{
"id": 1849628167,
"order_id": 2414345735,
"status": "success",
"created_at": "2016-01-20T17:26:58+05:30",
"service": "manual",
"updated_at": "2016-01-20T17:26:58+05:30",
"tracking_company": null,
"tracking_number": null,
"tracking_numbers": [],
"tracking_url": null,
"tracking_urls": [],
"receipt": {},
"line_items": [
{
"id": 4125768135,
"variant_id": 13917612359,
"title": "1001624/01",
"quantity": 1,
"price": "499.00",
"grams": 0,
"sku": "",
"variant_title": "Roman Bronze",
"vendor": "Boa casa",
"fulfillment_service": "manual",
"product_id": 4059739527,
"requires_shipping": true,
"taxable": true,
"gift_card": false,
"name": "1001624/01 - Roman Bronze",
"variant_inventory_management": "shopify",
"properties": [],
"product_exists": true,
"fulfillable_quantity": 0,
"total_discount": "0.00",
"fulfillment_status": "fulfilled",
"tax_lines": []
}
]
}
],
"refunds": [],
"customer": {
"id": 2324518599,
"email": "andrew@wyatt.co.in",
"accepts_marketing": false,
"created_at": "2016-01-20T17:18:35+05:30",
"updated_at": "2016-01-20T17:26:39+05:30",
"first_name": "Andrew",
"last_name": "Wyatt",
"orders_count": 1,
"state": "disabled",
"total_spent": "1018.00",
"last_order_id": 2414345735,
"note": "",
"verified_email": true,
"multipass_identifier": null,
"tax_exempt": false,
"tags": "",
"last_order_name": "#1005",
"default_address": {
"id": 2476804295,
"first_name": "Andrew",
"last_name": "Wyatt",
"company": "Wyatt Inc.",
"address1": "B-11, Betahouse",
"address2": "Street 11, Sector 52",
"city": "Manhattan",
"province": "New York",
"country": "United States",
"zip": "10027",
"phone": "145-112211",
"name": "Andrew Wyatt",
"province_code": "NY",
"country_code": "US",
"country_name": "United States",
"default": true
}
}
}
}

View File

@ -1,23 +0,0 @@
/* eslint-disable */
// rename this file from _test_[name] to test_[name] to activate
// and remove above this line
QUnit.test("test: Shopify Settings", function (assert) {
let done = assert.async();
// number of asserts
assert.expect(1);
frappe.run_serially([
// insert a new Shopify Settings
() => frappe.tests.make('Shopify Settings', [
// values to be set
{key: 'value'}
]),
() => {
assert.equal(cur_frm.doc.key, 'value');
},
() => done()
]);
});

View File

@ -1,107 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
import frappe
import unittest, os, json
from frappe.utils import cstr, cint
from erpnext.erpnext_integrations.connectors.shopify_connection import create_order
from erpnext.erpnext_integrations.doctype.shopify_settings.sync_product import make_item
from erpnext.erpnext_integrations.doctype.shopify_settings.sync_customer import create_customer
from frappe.core.doctype.data_import.data_import import import_doc
class ShopifySettings(unittest.TestCase):
@classmethod
def setUpClass(cls):
frappe.set_user("Administrator")
cls.allow_negative_stock = cint(frappe.db.get_value('Stock Settings', None, 'allow_negative_stock'))
if not cls.allow_negative_stock:
frappe.db.set_value('Stock Settings', None, 'allow_negative_stock', 1)
# use the fixture data
import_doc(path=frappe.get_app_path("erpnext", "erpnext_integrations/doctype/shopify_settings/test_data/custom_field.json"))
frappe.reload_doctype("Customer")
frappe.reload_doctype("Sales Order")
frappe.reload_doctype("Delivery Note")
frappe.reload_doctype("Sales Invoice")
cls.setup_shopify()
@classmethod
def tearDownClass(cls):
if not cls.allow_negative_stock:
frappe.db.set_value('Stock Settings', None, 'allow_negative_stock', 0)
@classmethod
def setup_shopify(cls):
shopify_settings = frappe.get_doc("Shopify Settings")
shopify_settings.taxes = []
shopify_settings.update({
"app_type": "Private",
"shopify_url": "test.myshopify.com",
"api_key": "17702c7c4452b9c5d235240b6e7a39da",
"password": "17702c7c4452b9c5d235240b6e7a39da",
"shared_secret": "17702c7c4452b9c5d235240b6e7a39da",
"price_list": "_Test Price List",
"warehouse": "_Test Warehouse - _TC",
"cash_bank_account": "Cash - _TC",
"account": "Cash - _TC",
"customer_group": "_Test Customer Group",
"cost_center": "Main - _TC",
"taxes": [
{
"shopify_tax": "International Shipping",
"tax_account":"Legal Expenses - _TC"
}
],
"enable_shopify": 0,
"sales_order_series": "SO-",
"sync_sales_invoice": 1,
"sales_invoice_series": "SINV-",
"sync_delivery_note": 1,
"delivery_note_series": "DN-"
}).save(ignore_permissions=True)
cls.shopify_settings = shopify_settings
def test_order(self):
# Create Customer
with open (os.path.join(os.path.dirname(__file__), "test_data", "shopify_customer.json")) as shopify_customer:
shopify_customer = json.load(shopify_customer)
create_customer(shopify_customer.get("customer"), self.shopify_settings)
# Create Item
with open (os.path.join(os.path.dirname(__file__), "test_data", "shopify_item.json")) as shopify_item:
shopify_item = json.load(shopify_item)
make_item("_Test Warehouse - _TC", shopify_item.get("product"))
# Create Order
with open (os.path.join(os.path.dirname(__file__), "test_data", "shopify_order.json")) as shopify_order:
shopify_order = json.load(shopify_order)
create_order(shopify_order.get("order"), self.shopify_settings, False, company="_Test Company")
sales_order = frappe.get_doc("Sales Order", {"shopify_order_id": cstr(shopify_order.get("order").get("id"))})
self.assertEqual(cstr(shopify_order.get("order").get("id")), sales_order.shopify_order_id)
# Check for customer
shopify_order_customer_id = cstr(shopify_order.get("order").get("customer").get("id"))
sales_order_customer_id = frappe.get_value("Customer", sales_order.customer, "shopify_customer_id")
self.assertEqual(shopify_order_customer_id, sales_order_customer_id)
# Check sales invoice
sales_invoice = frappe.get_doc("Sales Invoice", {"shopify_order_id": sales_order.shopify_order_id})
self.assertEqual(sales_invoice.rounded_total, sales_order.rounded_total)
# Check delivery note
delivery_note_count = frappe.db.sql("""select count(*) from `tabDelivery Note`
where shopify_order_id = %s""", sales_order.shopify_order_id)[0][0]
self.assertEqual(delivery_note_count, len(shopify_order.get("order").get("fulfillments")))

View File

@ -1,133 +0,0 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2015-10-05 16:55:20.455371",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 0,
"fields": [
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "shopify_tax",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Shopify Tax/Shipping Title",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_2",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "tax_account",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "ERPNext Account",
"length": 0,
"no_copy": 0,
"options": "Account",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2018-04-09 11:36:49.272815",
"modified_by": "Administrator",
"module": "ERPNext Integrations",
"name": "Shopify Tax Account",
"name_case": "",
"owner": "Administrator",
"permissions": [],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 0,
"track_seen": 0
}

View File

@ -1,10 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, 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 ShopifyTaxAccount(Document):
pass

View File

@ -1,103 +0,0 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2018-04-10 17:06:22.697427",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 0,
"engine": "InnoDB",
"fields": [
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "webhook_id",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Webhook ID",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "method",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Method",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2018-04-11 12:43:09.456449",
"modified_by": "Administrator",
"module": "ERPNext Integrations",
"name": "Shopify Webhook Detail",
"name_case": "",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0
}

View File

@ -1,10 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2018, 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 ShopifyWebhookDetail(Document):
pass

View File

@ -40,16 +40,6 @@
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Shopify Settings",
"link_to": "Shopify Settings",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
@ -113,4 +103,4 @@
"pin_to_bottom": 0,
"pin_to_top": 0,
"shortcuts": []
}
}

View File

@ -30,16 +30,6 @@
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Shopify Settings",
"link_to": "Shopify Settings",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
@ -79,4 +69,4 @@
"pin_to_bottom": 0,
"pin_to_top": 0,
"shortcuts": []
}
}

View File

@ -25,7 +25,8 @@ doctype_js = {
"Address": "public/js/address.js",
"Communication": "public/js/communication.js",
"Event": "public/js/event.js",
"Newsletter": "public/js/newsletter.js"
"Newsletter": "public/js/newsletter.js",
"Contact": "public/js/contact.js"
}
override_doctype_class = {
@ -340,7 +341,6 @@ scheduler_events = {
"erpnext.projects.doctype.project.project.hourly_reminder",
"erpnext.projects.doctype.project.project.collect_project_status",
"erpnext.hr.doctype.shift_type.shift_type.process_auto_attendance_for_all_shifts",
"erpnext.erpnext_integrations.connectors.shopify_connection.sync_old_orders",
"erpnext.support.doctype.service_level_agreement.service_level_agreement.set_service_level_agreement_variance"
],
"hourly_long": [
@ -437,7 +437,8 @@ regional_overrides = {
'erpnext.hr.utils.calculate_annual_eligible_hra_exemption': 'erpnext.regional.india.utils.calculate_annual_eligible_hra_exemption',
'erpnext.hr.utils.calculate_hra_exemption_for_period': 'erpnext.regional.india.utils.calculate_hra_exemption_for_period',
'erpnext.controllers.accounts_controller.validate_einvoice_fields': 'erpnext.regional.india.e_invoice.utils.validate_einvoice_fields',
'erpnext.assets.doctype.asset.asset.get_depreciation_amount': 'erpnext.regional.india.utils.get_depreciation_amount'
'erpnext.assets.doctype.asset.asset.get_depreciation_amount': 'erpnext.regional.india.utils.get_depreciation_amount',
'erpnext.stock.doctype.item.item.set_item_tax_from_hsn_code': 'erpnext.regional.india.utils.set_item_tax_from_hsn_code'
},
'United Arab Emirates': {
'erpnext.controllers.taxes_and_totals.update_itemised_tax_data': 'erpnext.regional.united_arab_emirates.utils.update_itemised_tax_data',

View File

@ -9,7 +9,7 @@ from frappe.utils import flt, getdate
from frappe import _
from frappe.model.mapper import get_mapped_doc
from frappe.model.document import Document
from erpnext.hr.utils import set_employee_name
from erpnext.hr.utils import set_employee_name, validate_active_employee
class Appraisal(Document):
def validate(self):
@ -19,6 +19,7 @@ class Appraisal(Document):
if not self.goals:
frappe.throw(_("Goals cannot be empty"))
validate_active_employee(self.employee)
set_employee_name(self)
self.validate_dates()
self.validate_existing_appraisal()

View File

@ -8,11 +8,13 @@ from frappe.utils import getdate, nowdate
from frappe import _
from frappe.model.document import Document
from frappe.utils import cstr, get_datetime, formatdate
from erpnext.hr.utils import validate_active_employee
class Attendance(Document):
def validate(self):
from erpnext.controllers.status_updater import validate_status
validate_status(self.status, ["Present", "Absent", "On Leave", "Half Day", "Work From Home"])
validate_active_employee(self.employee)
self.validate_attendance_date()
self.validate_duplicate_record()
self.validate_employee_status()

View File

@ -8,10 +8,11 @@ from frappe import _
from frappe.model.document import Document
from frappe.utils import date_diff, add_days, getdate
from erpnext.hr.doctype.employee.employee import is_holiday
from erpnext.hr.utils import validate_dates
from erpnext.hr.utils import validate_dates, validate_active_employee
class AttendanceRequest(Document):
def validate(self):
validate_active_employee(self.employee)
validate_dates(self, self.from_date, self.to_date)
if self.half_day:
if not getdate(self.from_date)<=getdate(self.half_day_date)<=getdate(self.to_date):

View File

@ -7,12 +7,13 @@ import frappe
from frappe import _
from frappe.utils import date_diff, add_days, getdate, cint, format_date
from frappe.model.document import Document
from erpnext.hr.utils import validate_dates, validate_overlap, get_leave_period, \
from erpnext.hr.utils import validate_dates, validate_overlap, get_leave_period, validate_active_employee, \
get_holidays_for_employee, create_additional_leave_ledger_entry
class CompensatoryLeaveRequest(Document):
def validate(self):
validate_active_employee(self.employee)
validate_dates(self, self.work_from_date, self.work_end_date)
if self.half_day:
if not self.half_day_date:

View File

@ -13,8 +13,10 @@ from frappe.model.document import Document
from erpnext.utilities.transaction_base import delete_events
from frappe.utils.nestedset import NestedSet
class EmployeeUserDisabledError(frappe.ValidationError): pass
class EmployeeLeftValidationError(frappe.ValidationError): pass
class EmployeeUserDisabledError(frappe.ValidationError):
pass
class InactiveEmployeeStatusError(frappe.ValidationError):
pass
class Employee(NestedSet):
nsm_parent_field = 'reports_to'
@ -196,7 +198,7 @@ class Employee(NestedSet):
message += "<br><br><ul><li>" + "</li><li>".join(link_to_employees)
message += "</li></ul><br>"
message += _("Please make sure the employees above report to another Active employee.")
throw(message, EmployeeLeftValidationError, _("Cannot Relieve Employee"))
throw(message, InactiveEmployeeStatusError, _("Cannot Relieve Employee"))
if not self.relieving_date:
throw(_("Please enter relieving date."))

View File

@ -7,7 +7,7 @@ import frappe
import erpnext
import unittest
import frappe.utils
from erpnext.hr.doctype.employee.employee import EmployeeLeftValidationError
from erpnext.hr.doctype.employee.employee import InactiveEmployeeStatusError
test_records = frappe.get_test_records('Employee')
@ -45,10 +45,33 @@ class TestEmployee(unittest.TestCase):
employee2_doc.save()
employee1_doc.reload()
employee1_doc.status = 'Left'
self.assertRaises(EmployeeLeftValidationError, employee1_doc.save)
self.assertRaises(InactiveEmployeeStatusError, employee1_doc.save)
def test_employee_status_inactive(self):
from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure
from erpnext.payroll.doctype.salary_structure.salary_structure import make_salary_slip
from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_holiday_list
employee = make_employee("test_employee_status@company.com")
employee_doc = frappe.get_doc("Employee", employee)
employee_doc.status = "Inactive"
employee_doc.save()
employee_doc.reload()
make_holiday_list()
frappe.db.set_value("Company", erpnext.get_default_company(), "default_holiday_list", "Salary Slip Test Holiday List")
frappe.db.sql("""delete from `tabSalary Structure` where name='Test Inactive Employee Salary Slip'""")
salary_structure = make_salary_structure("Test Inactive Employee Salary Slip", "Monthly",
employee=employee_doc.name, company=employee_doc.company)
salary_slip = make_salary_slip(salary_structure.name, employee=employee_doc.name)
self.assertRaises(InactiveEmployeeStatusError, salary_slip.save)
def tearDown(self):
frappe.db.rollback()
def make_employee(user, company=None, **kwargs):
""
if not frappe.db.get_value("User", user):
frappe.get_doc({
"doctype": "User",
@ -80,4 +103,5 @@ def make_employee(user, company=None, **kwargs):
employee.insert()
return employee.name
else:
frappe.db.set_value("Employee", {"employee_name":user}, "status", "Active")
return frappe.get_value("Employee", {"employee_name":user}, "name")

View File

@ -8,6 +8,7 @@ from frappe import _
from frappe.model.document import Document
from frappe.utils import flt, nowdate
from erpnext.accounts.doctype.journal_entry.journal_entry import get_default_bank_cash_account
from erpnext.hr.utils import validate_active_employee
class EmployeeAdvanceOverPayment(frappe.ValidationError):
pass
@ -18,11 +19,11 @@ class EmployeeAdvance(Document):
'make_payment_via_journal_entry')
def validate(self):
validate_active_employee(self.employee)
self.set_status()
def on_cancel(self):
self.ignore_linked_doctypes = ('GL Entry')
self.set_status()
def set_status(self):
if self.docstatus == 0:
@ -183,9 +184,9 @@ def make_return_entry(employee, company, employee_advance_name, return_amount,
bank_cash_account = get_default_bank_cash_account(company, account_type='Cash', mode_of_payment = mode_of_payment)
if not bank_cash_account:
frappe.throw(_("Please set a Default Cash Account in Company defaults"))
advance_account_currency = frappe.db.get_value('Account', advance_account, 'account_currency')
je = frappe.new_doc('Journal Entry')
je.posting_date = nowdate()
je.voucher_type = get_voucher_type(mode_of_payment)
@ -229,4 +230,4 @@ def get_voucher_type(mode_of_payment=None):
if mode_of_payment_type == "Bank":
voucher_type = "Bank Entry"
return voucher_type
return voucher_type

View File

@ -9,9 +9,11 @@ from frappe.model.document import Document
from frappe import _
from erpnext.hr.doctype.shift_assignment.shift_assignment import get_actual_start_end_datetime_of_shift
from erpnext.hr.utils import validate_active_employee
class EmployeeCheckin(Document):
def validate(self):
validate_active_employee(self.employee)
self.validate_duplicate_log()
self.fetch_shift()
@ -122,7 +124,7 @@ def mark_attendance_and_link_log(logs, attendance_status, attendance_date, worki
def calculate_working_hours(logs, check_in_out_type, working_hours_calc_type):
"""Given a set of logs in chronological order calculates the total working hours based on the parameters.
Zero is returned for all invalid cases.
:param logs: The List of 'Employee Checkin'.
:param check_in_out_type: One of: 'Alternating entries as IN and OUT during the same shift', 'Strictly based on Log Type in Employee Checkin'
:param working_hours_calc_type: One of: 'First Check-in and Last Check-out', 'Every Valid Check-in and Check-out'

View File

@ -50,28 +50,13 @@ frappe.ui.form.on('Employee Onboarding', {
}, __('Create'));
frm.page.set_inner_btn_group_as_primary(__('Create'));
}
if (frm.doc.docstatus === 1 && frm.doc.project) {
frappe.call({
method: "erpnext.hr.utils.get_boarding_status",
args: {
"project": frm.doc.project
},
callback: function(r) {
if (r.message) {
frm.set_value('boarding_status', r.message);
}
refresh_field("boarding_status");
}
});
}
},
employee_onboarding_template: function(frm) {
frm.set_value("activities" ,"");
if (frm.doc.employee_onboarding_template) {
frappe.call({
method: "erpnext.hr.utils.get_onboarding_details",
method: "erpnext.controllers.employee_boarding_controller.get_onboarding_details",
args: {
"parent": frm.doc.employee_onboarding_template,
"parenttype": "Employee Onboarding Template"

View File

@ -30,18 +30,14 @@
"fieldtype": "Link",
"label": "Job Applicant",
"options": "Job Applicant",
"reqd": 1,
"show_days": 1,
"show_seconds": 1
"reqd": 1
},
{
"fieldname": "job_offer",
"fieldtype": "Link",
"label": "Job Offer",
"options": "Job Offer",
"reqd": 1,
"show_days": 1,
"show_seconds": 1
"reqd": 1
},
{
"fetch_from": "job_applicant.applicant_name",
@ -49,116 +45,90 @@
"fieldtype": "Data",
"in_list_view": 1,
"label": "Employee Name",
"reqd": 1,
"show_days": 1,
"show_seconds": 1
"reqd": 1
},
{
"fieldname": "employee",
"fieldtype": "Link",
"label": "Employee",
"options": "Employee",
"read_only": 1,
"show_days": 1,
"show_seconds": 1
"read_only": 1
},
{
"fieldname": "date_of_joining",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Date of Joining",
"show_days": 1,
"show_seconds": 1
"label": "Date of Joining"
},
{
"allow_on_submit": 1,
"default": "Pending",
"fieldname": "boarding_status",
"fieldtype": "Select",
"label": "Status",
"options": "\nPending\nIn Process\nCompleted",
"show_days": 1,
"show_seconds": 1
"options": "Pending\nIn Process\nCompleted",
"read_only": 1
},
{
"allow_on_submit": 1,
"default": "0",
"fieldname": "notify_users_by_email",
"fieldtype": "Check",
"label": "Notify users by email",
"show_days": 1,
"show_seconds": 1
"label": "Notify users by email"
},
{
"fieldname": "column_break_7",
"fieldtype": "Column Break",
"show_days": 1,
"show_seconds": 1
"fieldtype": "Column Break"
},
{
"fieldname": "employee_onboarding_template",
"fieldtype": "Link",
"label": "Employee Onboarding Template",
"options": "Employee Onboarding Template",
"show_days": 1,
"show_seconds": 1
"options": "Employee Onboarding Template"
},
{
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company",
"show_days": 1,
"show_seconds": 1
"options": "Company"
},
{
"fieldname": "department",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Department",
"options": "Department",
"show_days": 1,
"show_seconds": 1
"options": "Department"
},
{
"fieldname": "designation",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Designation",
"options": "Designation",
"show_days": 1,
"show_seconds": 1
"options": "Designation"
},
{
"fieldname": "employee_grade",
"fieldtype": "Link",
"label": "Employee Grade",
"options": "Employee Grade",
"show_days": 1,
"show_seconds": 1
"options": "Employee Grade"
},
{
"fieldname": "project",
"fieldtype": "Link",
"label": "Project",
"options": "Project",
"read_only": 1,
"show_days": 1,
"show_seconds": 1
"read_only": 1
},
{
"fieldname": "table_for_activity",
"fieldtype": "Section Break",
"show_days": 1,
"show_seconds": 1
"fieldtype": "Section Break"
},
{
"allow_on_submit": 1,
"fieldname": "activities",
"fieldtype": "Table",
"label": "Activities",
"options": "Employee Boarding Activity",
"show_days": 1,
"show_seconds": 1
"options": "Employee Boarding Activity"
},
{
"fieldname": "amended_from",
@ -167,14 +137,12 @@
"no_copy": 1,
"options": "Employee Onboarding",
"print_hide": 1,
"read_only": 1,
"show_days": 1,
"show_seconds": 1
"read_only": 1
}
],
"is_submittable": 1,
"links": [],
"modified": "2020-06-25 15:22:24.923835",
"modified": "2021-06-03 18:01:51.097927",
"modified_by": "Administrator",
"module": "HR",
"name": "Employee Onboarding",

View File

@ -5,7 +5,7 @@
from __future__ import unicode_literals
import frappe
from frappe import _
from erpnext.hr.utils import EmployeeBoardingController
from erpnext.controllers.employee_boarding_controller import EmployeeBoardingController
from frappe.model.mapper import get_mapped_doc
class IncompleteTaskError(frappe.ValidationError): pass
@ -16,9 +16,9 @@ class EmployeeOnboarding(EmployeeBoardingController):
self.validate_duplicate_employee_onboarding()
def validate_duplicate_employee_onboarding(self):
emp_onboarding = frappe.db.exists("Employee Onboarding",{"job_applicant": self.job_applicant})
emp_onboarding = frappe.db.exists("Employee Onboarding", {"job_applicant": self.job_applicant})
if emp_onboarding and emp_onboarding != self.name:
frappe.throw(_("Employee Onboarding: {0} is already for Job Applicant: {1}").format(frappe.bold(emp_onboarding), frappe.bold(self.job_applicant)))
frappe.throw(_("Employee Onboarding: {0} already exists for Job Applicant: {1}").format(frappe.bold(emp_onboarding), frappe.bold(self.job_applicant)))
def validate_employee_creation(self):
if self.docstatus != 1:
@ -30,7 +30,7 @@ class EmployeeOnboarding(EmployeeBoardingController):
else:
task_status = frappe.db.get_value("Task", activity.task, "status")
if task_status not in ["Completed", "Cancelled"]:
frappe.throw(_("All the mandatory Task for employee creation hasn't been done yet."), IncompleteTaskError)
frappe.throw(_("All the mandatory tasks for employee creation are not completed yet."), IncompleteTaskError)
def on_submit(self):
super(EmployeeOnboarding, self).on_submit()

View File

@ -11,39 +11,26 @@ from erpnext.hr.doctype.employee_onboarding.employee_onboarding import Incomplet
from erpnext.hr.doctype.job_offer.test_job_offer import create_job_offer
class TestEmployeeOnboarding(unittest.TestCase):
def test_employee_onboarding_incomplete_task(self):
def setUp(self):
if frappe.db.exists('Employee Onboarding', {'employee_name': 'Test Researcher'}):
frappe.delete_doc('Employee Onboarding', {'employee_name': 'Test Researcher'})
_set_up()
applicant = get_job_applicant()
job_offer = create_job_offer(job_applicant=applicant.name)
job_offer.submit()
project = "Employee Onboarding : Test Researcher - test@researcher.com"
frappe.db.sql("delete from tabProject where name=%s", project)
frappe.db.sql("delete from tabTask where project=%s", project)
onboarding = frappe.new_doc('Employee Onboarding')
onboarding.job_applicant = applicant.name
onboarding.job_offer = job_offer.name
onboarding.company = '_Test Company'
onboarding.designation = 'Researcher'
onboarding.append('activities', {
'activity_name': 'Assign ID Card',
'role': 'HR User',
'required_for_employee_creation': 1
})
onboarding.append('activities', {
'activity_name': 'Assign a laptop',
'role': 'HR User'
})
onboarding.status = 'Pending'
onboarding.insert()
onboarding.submit()
def test_employee_onboarding_incomplete_task(self):
onboarding = create_employee_onboarding()
project_name = frappe.db.get_value("Project", onboarding.project, "project_name")
project_name = frappe.db.get_value('Project', onboarding.project, 'project_name')
self.assertEqual(project_name, 'Employee Onboarding : Test Researcher - test@researcher.com')
# don't allow making employee if onboarding is not complete
self.assertRaises(IncompleteTaskError, make_employee, onboarding.name)
# boarding status
self.assertEqual(onboarding.boarding_status, 'Pending')
# complete the task
project = frappe.get_doc('Project', onboarding.project)
for task in frappe.get_all('Task', dict(project=project.name)):
@ -51,6 +38,10 @@ class TestEmployeeOnboarding(unittest.TestCase):
task.status = 'Completed'
task.save()
# boarding status
onboarding.reload()
self.assertEqual(onboarding.boarding_status, 'Completed')
# make employee
onboarding.reload()
employee = make_employee(onboarding.name)
@ -61,6 +52,13 @@ class TestEmployeeOnboarding(unittest.TestCase):
employee.insert()
self.assertEqual(employee.employee_name, 'Test Researcher')
def tearDown(self):
for entry in frappe.get_all('Employee Onboarding'):
doc = frappe.get_doc('Employee Onboarding', entry.name)
doc.cancel()
doc.delete()
def get_job_applicant():
if frappe.db.exists('Job Applicant', 'Test Researcher - test@researcher.com'):
return frappe.get_doc('Job Applicant', 'Test Researcher - test@researcher.com')
@ -72,10 +70,35 @@ def get_job_applicant():
applicant.insert()
return applicant
def _set_up():
for doctype in ["Employee Onboarding"]:
frappe.db.sql("delete from `tab{doctype}`".format(doctype=doctype))
def get_job_offer(applicant_name):
job_offer = frappe.db.exists('Job Offer', {'job_applicant': applicant_name})
if job_offer:
return frappe.get_doc('Job Offer', job_offer)
project = "Employee Onboarding : Test Researcher - test@researcher.com"
frappe.db.sql("delete from tabProject where name=%s", project)
frappe.db.sql("delete from tabTask where project=%s", project)
job_offer = create_job_offer(job_applicant=applicant_name)
job_offer.submit()
return job_offer
def create_employee_onboarding():
applicant = get_job_applicant()
job_offer = get_job_offer(applicant.name)
onboarding = frappe.new_doc('Employee Onboarding')
onboarding.job_applicant = applicant.name
onboarding.job_offer = job_offer.name
onboarding.company = '_Test Company'
onboarding.designation = 'Researcher'
onboarding.append('activities', {
'activity_name': 'Assign ID Card',
'role': 'HR User',
'required_for_employee_creation': 1
})
onboarding.append('activities', {
'activity_name': 'Assign a laptop',
'role': 'HR User'
})
onboarding.status = 'Pending'
onboarding.insert()
onboarding.submit()
return onboarding

View File

@ -7,12 +7,11 @@ import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import getdate
from erpnext.hr.utils import update_employee
from erpnext.hr.utils import update_employee, validate_active_employee
class EmployeePromotion(Document):
def validate(self):
if frappe.get_value("Employee", self.employee, "status") != "Active":
frappe.throw(_("Cannot promote Employee with status Left or Inactive"))
validate_active_employee(self.employee)
def before_submit(self):
if getdate(self.promotion_date) > getdate():

View File

@ -7,9 +7,11 @@ import frappe
from frappe import _
from frappe.utils import get_link_to_form
from frappe.model.document import Document
from erpnext.hr.utils import validate_active_employee
class EmployeeReferral(Document):
def validate(self):
validate_active_employee(self.referrer)
self.set_full_name()
self.set_referral_bonus_payment_status()

View File

@ -23,27 +23,13 @@ frappe.ui.form.on('Employee Separation', {
frappe.set_route('List', 'Task', {project: frm.doc.project});
},__("View"));
}
if (frm.doc.docstatus === 1 && frm.doc.project) {
frappe.call({
method: "erpnext.hr.utils.get_boarding_status",
args: {
"project": frm.doc.project
},
callback: function(r) {
if (r.message) {
frm.set_value('boarding_status', r.message);
}
refresh_field("boarding_status");
}
});
}
},
employee_separation_template: function(frm) {
frm.set_value("activities" ,"");
if (frm.doc.employee_separation_template) {
frappe.call({
method: "erpnext.hr.utils.get_onboarding_details",
method: "erpnext.controllers.employee_boarding_controller.get_onboarding_details",
args: {
"parent": frm.doc.employee_separation_template,
"parenttype": "Employee Separation Template"

View File

@ -50,11 +50,12 @@
},
{
"allow_on_submit": 1,
"default": "Pending",
"fieldname": "boarding_status",
"fieldtype": "Select",
"label": "Status",
"options": "\nPending\nIn Process\nCompleted",
"reqd": 1
"options": "Pending\nIn Process\nCompleted",
"read_only": 1
},
{
"allow_on_submit": 1,
@ -147,7 +148,7 @@
],
"is_submittable": 1,
"links": [],
"modified": "2021-04-28 15:58:36.020196",
"modified": "2021-06-03 18:02:54.007313",
"modified_by": "Administrator",
"module": "HR",
"name": "Employee Separation",

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